Why Your Smart TV App Is Slow (and How to Fix It)
Tizen scrolling that jerks. WebOS FPS that won't break 30. A Fire TV Stick that crawls. A Samsung TV that runs out of memory after 10 minutes. Lightning animations that stutter. Notes for the engineer staring at Chrome DevTools at 11pm.
By Chris Lorenzo · Published May 16, 2026 · 12 min read
Whether you're searching for "Tizen app slow scrolling," "WebOS app low FPS," "Fire TV Stick JavaScript performance," "Samsung TV app memory leak," or "LightningJS garbage collection stutter," the platform name in your query is mostly irrelevant. The root causes are the same across all of them.
TV hardware is uniformly underpowered. A Fire TV Stick has a low-power ARM CPU and around 1GB of RAM. Tizen and WebOS run an aging Chromium on TV-class SoCs from five-year-old reference designs. Android TV boxes vary wildly but skew slow. The same React or Lightning code that holds 60 FPS on a MacBook can drop to 22 FPS on any of them, and you usually don't find out until you deploy to a test device.
Every TV performance problem I've debugged in the last three years falls into one of four root causes, plus the framework choice that sits underneath all of them. This guide walks through each one: what triggers it, how to spot it, and how to fix it. The patterns come from SolidTV and from forking the Lightning renderer for a 50% FPS improvement on low-end hardware.
What's in this guide
1. Start at the foundation: the framework you chose
On a MacBook, framework overhead is invisible. Every modern framework looks "fast enough" because the hardware does the work for you. On a Fire TV Stick with a 1.7GHz ARM core, that overhead is the performance budget. The choice you made on day one is the single largest performance decision in the project, and it's the hardest one to walk back later.
What the benchmarks actually say
The closest thing the JS ecosystem has to a neutral perf scoreboard is js-framework-benchmark. It scripts identical DOM-heavy workloads (create 1,000 rows, partial update, swap, clear) and measures them against a hand-written vanilla JS baseline of 1.00. Year over year, the ranking barely moves:
- SolidJS sits at roughly 1.05 to 1.10, within 5 to 10% of hand-written vanilla JavaScript. It has been the top-ranked mainstream framework on this benchmark for years.
- Svelte lands around 1.10 to 1.15.
- Vue 3 is around 1.20 to 1.30.
- React (with hooks) sits around 1.45 to 1.60. DOM-intensive operations take roughly 30 to 50% longer than they would in SolidJS for the same work.
That gap isn't a rounding error. On a desktop you'll never notice it. On a TV it's the difference between 45 FPS scrolling and 28 FPS scrolling, and it's there before you've written a single line of application code.
Why SolidJS is faster (it's structural, not magic)
The performance difference isn't a clever optimization React could "add later." It's a fundamentally different execution model:
- No Virtual DOM. React renders your component to a virtual tree, diffs it against the previous tree, then patches the real DOM. SolidJS compiles JSX directly into precise DOM operations at build time. There is no diff. There is no reconciliation step.
- Components run once. A React component re-executes its entire function body every time a parent re-renders. A SolidJS component runs exactly once. Only the specific reactive expressions inside it re-evaluate when their dependencies change.
- Fine-grained reactivity via signals. When a signal changes, SolidJS updates only the DOM node bound to that signal. React's default is the opposite: re-render everything, then memoize the parts you want to skip.
- No hook rules, no dependency arrays. Side effects are wired up once at component
creation, not re-evaluated every render. There's no
useMemooruseCallbacktax to remember to apply.
Bundle size: the second tax
Parsing and executing JavaScript is expensive on TV CPUs. Every kilobyte of framework code costs you startup time before your app even renders. Current minified and gzipped baselines:
- SolidJS: ~7 KB
- React + ReactDOM: ~44 KB combined
- SolidTV (SolidJS plus the Lightning integration layer): ~12 KB
- Blits (the framework from the Lightning team): ~90+ KB
On a Fire TV Stick, the difference between parsing 12 KB and parsing 90 KB at boot is measured in seconds, not milliseconds. We saw a 30% faster first paint in our SolidTV vs Blits head-to-head, mostly because of this. 2.5 seconds to first interactive vs 3.5 seconds, on the same hardware, same data, same TMDB workload.
If you're already on React
This isn't a "rewrite everything tomorrow" argument. But it should reframe how you think about optimization. Every fix in the rest of this guide is fighting against a framework that defaults to "do more work, then memoize it." A few specifics:
- React Native for TV sits on top of React's reconciler and pays the bridge cost on top of that. Teams shipping on it routinely settle for 30 FPS targets because they can't get higher without rewriting perf-critical screens in native.
- React-on-Lightning wrappers exist and "work," but you're paying React's reconciliation cost on top of Lightning's renderer cost. That's the worst of both worlds on a TV CPU.
- If a rewrite isn't on the table, the optimizations in the rest of this guide still apply. They just have a lower ceiling on React than on SolidJS. You can claw back the application-level waste, but you can't claw back the framework's per-update overhead.
The takeaway
If you're starting a new TV app, this is the easiest performance win you'll ever get: pick a framework that doesn't actively work against you. SolidJS is the fastest mainstream framework by a wide margin, and SolidTV is the integration that brings its model to Lightning's WebGL renderer. Skip the 30%+ React tax and start the rest of this guide with margin to spare.
2. You're rendering too much, too early
A "Browse" page with 8 rows × 30 tiles is 240 nodes. Add text inside each tile and you're past 480 render nodes before the user has even seen the screen. The TV happily eats the cost on first paint and then chokes when you try to translate one of those rows.
LazyRow / LazyColumn render upCount items immediately and append one more
each time the user navigates toward the edge. Good for typical content rails (10 to 50 items):
import { LazyRow } from '@solidtv/solid/primitives';
<LazyRow upCount={6} delay={250} each={items}>
{(item) => <Thumbnail {...item} />}
</LazyRow>
By rendering only what is needed on the screen, we keep the UI performant. As more nodes get added, the Renderer is only ever drawing the elements that should be visible. We slowly create more nodes as the user navigates, and the UI stays performant the whole time.
For lists in the thousands, VirtualRow / VirtualColumn goes further. It only ever has
displaySize + 2 × bufferSize children mounted, and reuses them as you scroll:
<VirtualRow displaySize={5} bufferSize={2} each={items}>
{(item) => <Thumbnail item={item()} />}
</VirtualRow>
See the full LazyRow docs and VirtualRow docs.
3. You're doing too much work during the keypress
Image prefetches, analytics events, telemetry pings, and state recalculations triggered inline on key down all compete with the scroll animation for the same frame. The remote feels laggy because it literally is.
scheduleTask. It
automatically pauses during keypresses and resumes when the renderer is idle:
import { scheduleTask } from '@solidtv/solid';
scheduleTask(() => warmImageCache(items), 'low');
scheduleTask(() => trackImpression(item), 'low');
Same behavior, but the animation stays smooth because the work happens in the gaps between key presses instead of on top of them.
This is also why the Lazy primitives from section 2 take a
delay option. After a keypress, the next node isn't created immediately; it waits for the
animation to finish first. Same principle: do the work before and after the animation, never during it.
4. Memory: GC stutter and slow leaks
SolidJS and the SolidTV Renderer have been heavily optimized to avoid GC. Objects are reused wherever possible, especially inside the render loop, so most of the pressure in a SolidTV app doesn't come from the framework.
The biggest source of garbage is almost always the API layer. When you get a response back from your
backend, don't convert it into other objects. Use the original response shape directly where you can, and
if the shape isn't workable, ask the API team to adjust the response rather than mapping it on the
client. A 200-item content rail that runs response.items.map(item => ({ id: item.uuid, name:
item.title, ... })) on every fetch generates a fresh object per item, with no reuse. That's the
kind of pattern that triggers a major GC mid-scroll on a TV CPU.
How to verify it
Both patterns show up in Chrome DevTools. The Chrome team has a thorough walkthrough at Fix memory problems. The short version:
- Open Chrome DevTools → Performance and record during the bad interaction.
- Watch the Memory graph. A rhythmic sawtooth is GC pressure. A line that climbs and never comes back down is a leak.
- For leaks, switch to the Memory panel and diff heap snapshots taken before and after the suspect flow to see which objects survived.
Common leak sources
A few patterns that come up in audits, in rough order of frequency:
- Event listeners attached and never removed. Custom focus, keyboard, or
remote-control handlers attached to a page that's still referenced after navigation. Every navigation
adds another, and after ten navigations every key press fires ten handlers. Use
onCleanupto register teardown alongside the listener.useFocusManagerhandles this for you; globaldocument.addEventListenercalls need it explicitly. KeepAliveRoutecaching everything forever. Cache too many pages and you've replaced a slowdown with a leak. UseshouldDisposeor callremoveKeepAlive(id)manually when a route isn't worth keeping.- Effects on cached routes that never stop running. When a
KeepAliveRouteis hidden, its reactive context stays alive and effects keep firing. Gate withprops.isAlive()so background pages stop fetching and stop updating signals. - Image textures that never release. Posters and hero images occupy GPU memory until the node referencing them is destroyed. Size images for the screen (no 4K poster for a 220px tile), and destroy cached pages with large image grids under memory pressure.
If you've cleaned up the API layer and the sawtooth persists, the allocations are coming from the renderer itself. See Boosting LightningJS FPS by 50% for the renderer-level work that addresses it.
Step 0: Get a Number Before You Start
None of the fixes above matter if you can't measure the problem. "Feels slow on the box" doesn't get fixed.
The fastest way to find out what your TV is actually capable of is to load the SolidTV benchmark demo on the device itself. It's a standardized SolidTV scrolling and animation workload, so the FPS it reports is a clean ceiling for the hardware: if even the benchmark can't hold 60 on your target TV, that's the floor your own app is fighting against. If the benchmark hits 60 and your app sits at 28, the problem is in your code, not the device.
Once you have that baseline, drop SolidTV's
<FPSCounter>
into the corner of your own app for ongoing measurement:
import { FPSCounter, setupFPS } from '@solidtv/solid/primitives';
import { renderer } from '@solidtv/solid';
setupFPS({ renderer });
<FPSCounter mountX={1} x={1910} y={10} />
Now you know whether idle FPS is 60, 45, or 28, and which interactions tank it. Then attach the Chromium remote debugger (Tizen, WebOS, Fire TV, and Android TV all expose one) and capture a 5-second performance recording during the bad interaction. You'll almost always see one of: long JavaScript tasks blocking the render thread, the GC sawtooth from section 4, or slow paint times from too many shader switches or oversized textures.
The Diagnostic Checklist
When a TV app feels slow, regardless of which platform surfaced it, work through this in order:
- Mount an FPS counter on the device. Get a number.
- Profile on the real hardware, not the simulator and not your laptop.
- Virtualize long lists with
LazyRoworVirtualRow. - Defer non-critical work with
scheduleTask. - Audit the API layer for allocations: avoid re-shaping every response into new objects; use the original response where you can.
- Size images for the screen, not the source asset.
- If you've done all of the above and it's still slow, buy your customer a new TV.
SolidTV bakes most of this in by default. It's the framework I wanted while I was debugging stuttery Lightning apps in production. If you'd rather not assemble the toolkit yourself, start here.