Skip to content

Polyfilling React & Next.js for UK TV and Set-Top Box Browsers

Posted on:February 26, 2026
· 12 min read

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.)

Freeview Play (HbbTV)

Samsung Tizen

LG webOS

Sky Q

Sky Glass

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:

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:

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:

  1. Get a Humax box - They’re cheap used on eBay
  2. Use the HbbTV validator - validator.hbbtv.org
  3. Remote debugging - Most STBs support debug mode via network
  4. BrowserStack - Has some smart TV browsers

For the DTRT2100 specifically:

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:

  1. Target ES5 - Use Babel with explicit browser targets
  2. Polyfill everything - Don’t assume any ES6+ features exist
  3. Test on hardware - Simulators miss device-specific quirks
  4. Optimise bundle size - Memory limits are real
  5. 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:

The streaming world moves slowly. Plan accordingly.