DISPATCH · TV PLATFORM · 6 MIN READ
Sixty Frames on a Cabin TV
xtv-platform is the TV side of what we build at Xcontrol: the guest-facing apps running on the panels in cabins and hotel rooms. Channel rails, program guides, service menus, the interface a guest meets when they pick up the remote. Under the vendor wrappers they're web apps, and for years they were built the way web apps are built: components rendering into the DOM.
On a developer laptop that's fine. On the actual panel it never quite was. Rails stuttered when you scrolled them. Remote presses landed with a visible delay. Boot was long enough that guests pressed the button twice. Nobody had filed a ticket called "the platform feels cheap", and no migration was on the roadmap. I put it there.
The problem was the DOM itself
A hospitality panel is not a laptop. It's a low-power SoC running a Chromium build that's often years old, with a fraction of the CPU and memory a phone gets. Every DOM mutation drags the full pipeline behind it: style recalculation, layout, paint, composite. A poster rail with a few dozen tiles animating at once asks that pipeline to run per frame, and the panel simply can't. You can virtualise, you can throttle, you can will-change your way to small wins, and we did. The ceiling stays where the DOM put it.
Lightning Blits takes the pipeline out of the equation. It's the app framework on top of Lightning 3, the open-source renderer Comcast built for exactly this class of device. Components are declarative templates with reactive props, so the developer experience feels familiar, but nothing renders to the DOM. The whole UI is a WebGL scene graph: textures on a canvas, animated on the GPU. No layout, no reflow, no style recalculation, ever.
Nobody asked for a prototype, so I built one
Arguments about frameworks go nowhere; demos on real hardware end them. I took our heaviest screen, the channel rail with live poster art, and rebuilt it in Blits. Same data, same design, same panel. Then I put the two versions side by side in front of the team and handed someone the remote.
The DOM version did what it always did: dropped frames on every scroll, hesitated on every keypress. The Blits version tracked the remote like a native app. The numbers, measured on the panel and not on anyone's laptop:
METRIC DOM APP BLITS PROTOTYPE cold boot ~8 s ~2.5 s rail scroll janky steady 60 fps JS heap after nav ~190 MB ~75 MB
That meeting ended with the migration on the roadmap. Not because anyone loved the idea of rewriting working screens, but because the gap was too large to defend keeping.
Widgets first, screens second
A migration where every developer learns WebGL texture management from scratch dies in the first sprint. So before any screen moved, I built the widget kit: the reusable Blits components that encode the hard-won TV knowledge once, so building a screen becomes assembly.
- Focus-aware rails and grids. Spatial navigation, wrap rules, and scroll physics handled inside the widget. A screen declares what's in the rail; the widget decides how the remote moves through it.
- Poster tiles with texture discipline. Lazy texture upload as tiles approach the viewport, release when they leave. GPU texture memory is the real budget on a panel, and the widget enforces it so screens can't leak.
- Video surface. WebGL draws over the native video plane, so live TV is a punch-through: a transparent hole in the canvas with the tuner behind it. One widget owns that trick; nobody else has to know it exists.
- On-screen keyboard, toasts, dialogs. The unglamorous furniture every screen needs, focus-managed and themed once.
- Theme tokens. Same design tokens across old and new screens, so the migration is invisible to guests while it's in flight.
With the kit in place the migration ran as a strangler: screen by screen, new Blits screens shipping alongside remaining DOM ones, always a rollback path. Each converted screen inherited the performance profile for free, because the widgets carry it.
Measured where guests actually watch
Every number above came off real panels, not desktop Chrome, and staying honest about that is its own discipline. This is the same itch that later became tv-mcp: the loop of installing on a physical TV, screenshotting it, reading its console, and evaluating state inside the running app, without walking to the screen. Measure on the device the guest holds the remote for. Desktop numbers on TV projects are fiction.
What the migration taught me
- Initiative is a prototype, not a proposal. One rebuilt screen on real hardware did what no slide deck could.
- Platform migrations scale through shared components, not documentation. Encode the hard knowledge in widgets and the tenth screen costs a fraction of the first.
- On constrained devices, the rendering model is the performance ceiling. Optimising inside the wrong model is polishing a bottleneck.
- Keep the old path alive until the new one has earned its keep. Strangler beats big bang, on ships especially.
If you're building UIs for TVs, set-top boxes, or anything else where the hardware fights back, I'd be glad to compare notes: email or LinkedIn.