Building streaming apps for UK televisions means supporting browsers that mainstream web development forgot years ago. The YouView Humax DTRT2100, Freeview Play boxes, and various smart TV platforms run JavaScript engines that predate ES6 - sometimes significantly. Here’s how to make modern React and Next.js applications work on these devices.
The UK TV Browser Landscape
Before diving into solutions, understand what you’re dealing with:
YouView/Humax Devices (DTRT2100, etc.)
- Opera-based browser (often Opera Devices SDK)
- JavaScript engine varies by firmware - some stuck on ES5
- Limited memory (~256-512MB shared with OS)
- No native Fetch API
- Inconsistent Promise support
Freeview Play (HbbTV)
- HbbTV 2.0.1 spec mandates Opera or a CEF derivative
- ES5 baseline, some ES6 features via firmware updates
- OIPF (Open IPTV Forum) DAE APIs alongside standard DOM
Samsung Tizen
- Chromium-based, relatively modern
- Older models (2016-2018) still on Chrome 47-56 equivalent
LG webOS
- Chromium-based (varies by year)
- 2016-2018 models comparable to Chrome 38-53
Sky Q
- Custom Linux OS with Opera Devices SDK browser
- ES5 + limited ES6
- Weak GPU (~256MB VRAM shared with system)
- Released 2016, architecture largely frozen
- Significant performance constraints on animations and image-heavy UIs
Sky Glass
- Runs Comcast’s Global Video Platform (same as Xfinity)
- Modern Chromium-based browser with proper ES6+ support
- Released 2021, significantly more capable hardware
- Despite sharing the Sky brand, it’s a completely different platform
The common denominator: assume ES5 with spotty ES6 support. Build for the lowest target.
Core Polyfill Strategy
1. core-js: The Foundation
core-js is the most comprehensive polyfill library. It covers ES5 through ES2023+ features modularly.
npm install core-js
For maximum compatibility, import at your app’s entry point:
// Entry point - before any other code
import 'core-js/stable';
import 'regenerator-runtime/runtime';
For smaller bundles, import only what you need:
import 'core-js/features/promise';
import 'core-js/features/array/includes';
import 'core-js/features/object/assign';
import 'core-js/features/string/includes';
import 'core-js/features/symbol';
import 'core-js/features/map';
import 'core-js/features/set';
2. regenerator-runtime for Async/Await
Async functions compile to generators, which need runtime support on ES5:
npm install regenerator-runtime
import 'regenerator-runtime/runtime';
This is often bundled with @babel/preset-env but for TV targets, be explicit.
Next.js Polyfill Configuration
Built-in Polyfills
Next.js includes some polyfills automatically:
fetch()(whatwg-fetch + node-fetch)URLandURLSearchParamsObject.assign
But this isn’t enough for TV browsers.
Custom Polyfill Entry
Create a dedicated polyfill file that loads before everything else:
// polyfills.js
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import 'whatwg-fetch';
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch';
import 'url-polyfill';
import 'intersection-observer';
import 'resize-observer-polyfill';
Import at the top of _app.js or _app.tsx:
// pages/_app.js
import '../polyfills';
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
Babel Configuration
Next.js uses SWC by default now, but for TV targets you might need Babel for finer control:
// babel.config.js
module.exports = {
presets: [
['next/babel', {
'preset-env': {
useBuiltIns: 'usage',
corejs: 3,
targets: {
browsers: [
'Chrome >= 38',
'Safari >= 9',
'Firefox >= 45',
'Opera >= 30',
'ie >= 11'
]
}
}
}]
]
};
The useBuiltIns: 'usage' option automatically imports polyfills based on what your code uses. But for TV deployment, I recommend 'entry' mode with explicit imports - it’s more predictable.
next.config.js Transpilation
Force transpilation of node_modules that ship ES6+:
// next.config.js
const nextConfig = {
transpilePackages: [
'some-es6-module',
'another-modern-package'
],
webpack: (config, { isServer }) => {
if (!isServer) {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
net: false,
tls: false
};
}
return config;
}
};
module.exports = nextConfig;
Essential Polyfills for TV Browsers
Fetch API
Most TV browsers lack native Fetch:
npm install whatwg-fetch
import 'whatwg-fetch';
For abort controller support (needed for request cancellation):
npm install abortcontroller-polyfill
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch';
Promises
Some Humax firmware has broken Promise implementations:
npm install promise-polyfill
import 'promise-polyfill/src/polyfill';
Or via core-js:
import 'core-js/features/promise';
import 'core-js/features/promise/finally';
import 'core-js/features/promise/all-settled';
IntersectionObserver
Used by lazy loading libraries and infinite scroll. Not available on any TV browser by default:
npm install intersection-observer
import 'intersection-observer';
ResizeObserver
Used by many UI libraries for responsive behavior:
npm install resize-observer-polyfill
import ResizeObserver from 'resize-observer-polyfill';
if (!window.ResizeObserver) {
window.ResizeObserver = ResizeObserver;
}
MutationObserver
Some older Opera builds need this:
npm install mutationobserver-shim
import 'mutationobserver-shim';
requestAnimationFrame
Crucial for animations, missing on some STBs:
// raf-polyfill.js
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
window.requestAnimationFrame = window[vendors[i] + 'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[i] + 'CancelAnimationFrame']
|| window[vendors[i] + 'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function(callback) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() {
callback(currTime + timeToCall);
}, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}
})();
Array Methods
ES5 TV browsers often miss later array additions:
import 'core-js/features/array/includes';
import 'core-js/features/array/find';
import 'core-js/features/array/find-index';
import 'core-js/features/array/from';
import 'core-js/features/array/flat';
import 'core-js/features/array/flat-map';
Object Methods
import 'core-js/features/object/assign';
import 'core-js/features/object/entries';
import 'core-js/features/object/values';
import 'core-js/features/object/from-entries';
String Methods
import 'core-js/features/string/includes';
import 'core-js/features/string/starts-with';
import 'core-js/features/string/ends-with';
import 'core-js/features/string/pad-start';
import 'core-js/features/string/pad-end';
import 'core-js/features/string/trim-start';
import 'core-js/features/string/trim-end';
Map, Set, WeakMap, WeakSet
React and many libraries rely on these:
import 'core-js/features/map';
import 'core-js/features/set';
import 'core-js/features/weak-map';
import 'core-js/features/weak-set';
Symbol
Required for iterators and many modern patterns:
import 'core-js/features/symbol';
import 'core-js/features/symbol/iterator';
React-Specific Considerations
React 18 and TV Browsers
React 18’s concurrent features need modern browser APIs. For TV targets, disable concurrent rendering:
// Use createRoot but without concurrent features
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
If you hit issues, fall back to React 17’s render:
import { render } from 'react-dom';
render(<App />, document.getElementById('root'));
React Polyfills
React itself needs:
// Required for React
import 'core-js/features/map';
import 'core-js/features/set';
import 'core-js/features/object/assign';
import 'core-js/features/symbol';
import 'core-js/features/array/from';
The React team documents these requirements at react.dev/link/react-polyfills.
Event Handling
Some TV browsers have quirky event handling. Consider:
npm install events-polyfill
Conditional Loading Strategy
Loading all polyfills for every user wastes bandwidth. Use differential serving:
1. Feature Detection Bundle
Create a loader that checks capabilities:
// polyfill-loader.js
var needsPolyfills = (
typeof Promise === 'undefined' ||
typeof fetch === 'undefined' ||
typeof Symbol === 'undefined' ||
typeof Object.assign === 'undefined' ||
!Array.prototype.includes
);
if (needsPolyfills) {
var script = document.createElement('script');
script.src = '/polyfills.bundle.js';
script.onload = function() { window.initApp(); };
document.head.appendChild(script);
} else {
window.initApp();
}
2. User-Agent Based Loading
For known devices, load polyfills by UA:
// server-side or in middleware
const tvUserAgents = [
/HbbTV/i,
/YouView/i,
/HUMAX/i,
/Freeview/i,
/SmartTV/i,
/Tizen/i,
/WebOS/i,
/BRAVIA/i
];
function needsTVPolyfills(userAgent) {
return tvUserAgents.some(regex => regex.test(userAgent));
}
3. Next.js Middleware Approach
// middleware.js
import { NextResponse } from 'next/server';
export function middleware(request) {
const ua = request.headers.get('user-agent') || '';
const isTV = /HbbTV|YouView|HUMAX|SmartTV|Tizen|WebOS/.test(ua);
const response = NextResponse.next();
response.headers.set('x-device-type', isTV ? 'tv' : 'standard');
return response;
}
Then in _document.js:
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document({ __NEXT_DATA__ }) {
const isTV = /* get from headers or cookies */;
return (
<Html>
<Head>
{isTV && <script src="/tv-polyfills.js" />}
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
Memory Considerations
TV browsers have tight memory limits. Optimise polyfills:
1. Tree Shaking
Use specific imports, not the entire library:
// Bad - imports everything
import 'core-js';
// Good - imports only what's needed
import 'core-js/features/promise';
import 'core-js/features/array/includes';
2. Deferred Loading
Load non-critical polyfills after initial render:
useEffect(() => {
if (!('IntersectionObserver' in window)) {
import('intersection-observer').then(() => {
// Now safe to use lazy loading
});
}
}, []);
3. Bundle Analysis
Check polyfill impact on bundle size:
npm install @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true'
});
module.exports = withBundleAnalyzer(nextConfig);
Run with ANALYZE=true npm run build.
GPU Limitations and Animation Performance
Beyond JavaScript polyfills, TV browsers - especially Sky Q - have severe GPU constraints that cause performance issues with image-heavy UIs and animations.
The Problem
Sky Q and similar STBs have GPUs with approximately 256MB VRAM shared with the system. When building content rails (horizontal carousels of show thumbnails, brand logos, etc.), you quickly hit limits:
- Each image in a rail gets composited as a separate GPU layer
- CSS transforms and animations promote elements to GPU layers
- GPU memory exhausts → frame drops, stuttering, white boxes, or crashes
Optimisation Strategies
1. Virtualise Rails - Only Render What’s Visible
Don’t render a 50-item rail and hide overflow. Only mount items that are on-screen plus 1-2 offscreen for smooth scrolling:
// Pseudo-code for virtualised rail
const visibleStart = Math.floor(scrollPosition / itemWidth);
const visibleEnd = visibleStart + itemsPerScreen + 2;
return items.slice(visibleStart, visibleEnd).map(item => (
<RailItem key={item.id} style={{ transform: `translateX(${item.index * itemWidth}px)` }} />
));
Libraries like react-window or react-virtualized handle this, but you may need a custom implementation for TV navigation patterns.
2. Serve Appropriately Sized Images
A 1920px hero image for a 300px thumbnail murders VRAM. Detect Sky Q via User-Agent and serve smaller assets:
function getImageSize(userAgent) {
if (/Sky_Q/.test(userAgent)) {
return { width: 400, quality: 70 };
}
return { width: 800, quality: 85 };
}
3. Remove will-change and Layer Promotion Hacks
On desktop, will-change: transform hints to pre-composite for smoother animations. On Sky Q, it just allocates GPU memory for every element:
/* Remove these for TV builds */
.rail-item {
/* will-change: transform; */
/* transform: translateZ(0); */
}
4. Avoid Opacity Animations
Opacity changes force compositing. Use visibility toggles or class swaps instead:
/* Bad - forces compositing */
.item { transition: opacity 0.3s; }
.item.hidden { opacity: 0; }
/* Better - no GPU overhead */
.item.hidden { visibility: hidden; }
5. Limit Concurrent Animations
One rail animating at a time, not three. Queue animations or disable them entirely on constrained devices:
const isLowEndDevice = /Sky_Q|HUMAX/.test(navigator.userAgent);
const transitionDuration = isLowEndDevice ? 0 : 300;
6. Throttle requestAnimationFrame
Sky Q’s GPU can’t sustain 60fps. Throttle to 30fps:
let lastFrame = 0;
const targetFPS = isLowEndDevice ? 30 : 60;
const frameInterval = 1000 / targetFPS;
function animate(timestamp) {
if (timestamp - lastFrame >= frameInterval) {
lastFrame = timestamp;
// Do animation work
}
requestAnimationFrame(animate);
}
7. Prefer JPG Over PNG
PNGs take more GPU memory to decode. Use JPG for photos and only PNG when you genuinely need transparency:
function getImageFormat(hasTransparency, userAgent) {
if (hasTransparency) return 'png';
if (/Sky_Q|HUMAX/.test(userAgent)) return 'jpg'; // Smaller decode footprint
return 'webp'; // Modern devices
}
8. Consider a “Lite” UI Mode
Some teams ship a completely simplified UI to Sky Q - no animations, simpler rails, fewer images loaded at once. It’s not pretty, but it works:
// In your app config or context
const uiMode = detectDevice(userAgent);
// 'full' | 'lite'
if (uiMode === 'lite') {
// Disable animations
// Reduce items per rail
// Lower image quality
// Simplify transitions
}
Detecting Sky Q and Other Constrained Devices
User-Agent detection for UK STBs:
const constrainedDevices = [
/Sky_Q/i,
/HUMAX/i,
/YouView/i,
/Freeview/i,
/HbbTV.*Opera/i // Older HbbTV with Opera
];
function isConstrainedDevice(userAgent) {
return constrainedDevices.some(regex => regex.test(userAgent));
}
Sky Glass uses a different UA and doesn’t need these constraints - it’s essentially a modern Chromium browser.
Testing on Actual Devices
Simulators lie. Test on real hardware:
- Get a Humax box - They’re cheap used on eBay
- Use the HbbTV validator - validator.hbbtv.org
- Remote debugging - Most STBs support debug mode via network
- BrowserStack - Has some smart TV browsers
For the DTRT2100 specifically:
- Enable developer mode in hidden settings
- Connect via IP for remote console
- Check firmware version - behavior varies significantly
Complete Polyfill Bundle Example
Here’s a production-ready TV polyfill setup:
// tv-polyfills.js
// Load order matters!
// 1. Core language features
import 'core-js/stable';
import 'regenerator-runtime/runtime';
// 2. DOM APIs
import 'whatwg-fetch';
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch';
import 'url-polyfill';
// 3. Observers
import 'intersection-observer';
import ResizeObserver from 'resize-observer-polyfill';
if (!window.ResizeObserver) window.ResizeObserver = ResizeObserver;
import 'mutationobserver-shim';
// 4. Animation
import './raf-polyfill';
// 5. Events
import 'events-polyfill';
// 6. Custom Elements (if using web components)
import '@webcomponents/custom-elements';
// 7. CSS (if using CSS custom properties on old browsers)
import 'css-vars-ponyfill';
console.log('[Polyfills] TV compatibility layer loaded');
Summary
Building for UK TV browsers means:
- Target ES5 - Use Babel with explicit browser targets
- Polyfill everything - Don’t assume any ES6+ features exist
- Test on hardware - Simulators miss device-specific quirks
- Optimise bundle size - Memory limits are real
- Consider differential serving - Don’t punish modern browsers
The complexity is annoying, but it’s the reality of broadcast streaming platforms. These devices have 7+ year lifecycles - your 2026 app needs to run on 2019 hardware running 2016 browsers.
Libraries to know:
- core-js - ES feature polyfills
- regenerator-runtime - Async/await support
- whatwg-fetch - Fetch API
- intersection-observer - Lazy loading support
- resize-observer-polyfill - Responsive components
- url-polyfill - URL/URLSearchParams
The streaming world moves slowly. Plan accordingly.