Web App (HTML5) Memory Optimization Guide

Building memory-efficient web apps on constrained TV hardware

Audience

Third-party developers building web apps (HTML5) for TV

Purpose

Principles and practices for using memory efficiently on constrained TV hardware

Runtime

A Chromium-based web engine on the TV (Blink renderer + V8 JavaScript engine)

Nature of this document: This is a best-practice guide. It is not intended to mandate compliance or to trigger any action based on compliance — use it as a reference for better app quality and user experience.


Contents

  1. Why memory matters for TV web apps
  2. Understanding the memory model of web apps
  3. Image and graphics memory optimization
  4. DOM management and list/grid virtualization
  5. Preventing memory leaks
  6. JavaScript memory and GC management
  7. Video & media memory management
  8. Data handling optimization (fetch / JSON / cache)
  9. Canvas / WebGL / Web Audio / Worker cautions

1. Why memory matters for TV web apps

TVs have different memory constraints than smartphones or PCs. Understanding this difference is the starting point of optimization.

  • Memory is a shared resource. When one app uses too much memory, it isn't only that app that slows down — the whole TV degrades. App switching gets slower, other apps in the background get killed, and system animations stutter.

Core principle: Memory is managed, not borrowed. Make "allocate when needed, release the moment it's no longer needed" the default for every feature you design.


2. Understanding the memory model of web apps

To optimize, you first need to know where a web app's memory goes. Unlike native apps, a web app's memory is spread across several layers, much of which is not directly visible to the developer.

2.1 Major memory-consuming areas

Area

Description

Developer control

V8 JS heap

JavaScript objects, arrays, closures, functions, etc.

High

DOM / render tree

DOM nodes, render objects, computed style

High

Decoded images

Not the compressed file, but the bitmap expanded for display

High

GPU memory (textures/layers)

Compositing layers, textures, canvas back buffer

Medium

Media buffers

Video/audio decode buffers, MSE SourceBuffer

Medium

Network & cache

HTTP cache, fetch responses, string buffers

Medium

Engine overhead

V8/Blink internal structures, fonts, parsers, etc.

Low

2.2 "Compressed file size" is not "memory usage"

This is one of the most important concepts for understanding memory usage.

  • Even a 100 KB JPEG must be decompressed into a bitmap to be displayed.
  • Most pixels take 4 bytes (RGBA). So a single 1920×1080 image = about 8.3 MB of decode memory. (1920 × 1080 × 4 ≈ 8,294,400 bytes)
  • A single 4K (3840×2160) image is about 33 MB.
  • "It's a small file, so it's fine" is a misconception. The real memory is the number of pixels actually drawn on screen × 4 bytes.

The same applies to JSON. When you JSON.parse() a 200 KB JSON string, the original string stays in memory while the parsed object tree is created on top of it. The parse result can take several times more memory than the original.

2.3 The V8 heap and garbage collection (GC)

  • JavaScript is a GC language. GC only reclaims objects that are no longer reachable (their references are cut).
  • Conversely, if a reference remains anywhere, the object is never reclaimed. This is the root cause of web app memory leaks.
  • GC isn't free. While GC runs, the main thread can briefly pause, which shows up as animation frame drops.
  • V8 uses a generational GC. Newly created objects are reclaimed cheaply in the young generation, so creating and discarding temporary objects in ordinary code is not itself a problem. The place to watch is hot paths that run tens to hundreds of times per second (animation loops, scroll/input handlers, tight loops). Creating a new object every time there makes even a cheap GC run frequently, causing frame jank. So the fix is not to reduce allocations blindly, but to reuse objects only in hot paths (see 6.1).
  • The cause of a leak is not "circular references" per se, but "something is still referencing it, so GC can't reclaim it." JavaScript's GC reclaims even a cyclic structure (A→B, B→A) correctly once nothing outside references it; the same is true for cycles involving DOM nodes. Real leaks happen when global variables, long-lived arrays/objects, closures, or unremoved event listeners keep holding an object (see Section 5).

3. Image and graphics memory optimization

Images are the single largest memory consumer in almost every TV web app. Just following the principles in this section resolves most memory problems.

Avoid animated GIFs — they use a lot of memory. Use CSS animations for simple UI motion, and if you truly need raster animation, use animated WebP instead of GIF.

3.1 Load images at the size you display (most important)

The key is not to download an image larger than the size you display and shrink it on the device.

  • Don't shrink a large original into a small area. Putting a 1920×1080 original into a 200×300 thumbnail slot looks small on screen, but you still download that large original and decode it at least once — wasting network, CPU, and peak memory.
  • Download images resized on the server to the display size. Thumbnails at thumbnail size, backgrounds at background size.
  • Don't count on the browser to shrink it for you. The browser sometimes downscales an image to the display size, but not always, and the cost of downloading and expanding the large original is incurred regardless. The reliable way to save memory is to receive an image that is already at the display size.
<!-- Bad: using a 4K original for a thumbnail -->
<img src="poster_4k.jpg" style="width:200px; height:300px">
<!-- → Small on screen, but the 4K original is still downloaded and decoded: wasted network / CPU / memory -->

<!-- Good: serve an image sized for display from the server -->
<img src="poster_200x300.jpg" width="200" height="300">

3.2 Don't load offscreen images; release them when not visible

  • Lazy loading: Don't load images not yet visible from scrolling. Load them only as they approach the viewport, via the loading="lazy" attribute or IntersectionObserver.
<img src="poster.jpg" loading="lazy" width="200" height="300" alt="...">
  • Actively release images that leave the screen: In a long TV grid, reclaim decode memory for images far from the viewport by clearing src or removing the element. (See Section 4 on virtualization.)
// Release image memory for a card far from the viewport
function releaseImage(imgEl) {
  imgEl.removeAttribute('src');   // or imgEl.src = '';
  imgEl.removeAttribute('srcset');
}
  • Applying content-visibility: auto to large offscreen sections can defer their rendering/layout cost and associated memory.
.offscreen-section {
  content-visibility: auto;
  contain-intrinsic-size: 400px 300px; /* size hint */
}

3.3 Treat CSS background images the same way

An image set via background-image also uses bitmap memory once decoded. Background images of hidden elements can still load, so make large backgrounds load only when shown — by attaching a class at that moment.

3.4 Control decode timing

  • Suddenly attaching a large image to the screen can trigger a synchronous decode on the main thread and cause jank. Use the decoding="async" attribute or the img.decode() promise to decode ahead of time, asynchronously, before display.
const img = new Image();
img.src = 'poster.jpg';
await img.decode();         // after decode completes
container.appendChild(img); // attach without a frame drop

3.5 Watch out for web fonts

Web fonts can be as large a single consumer as images, yet are often overlooked. The browser keeps a font's glyphs resident in memory, and receiving a separate file per weight/style multiplies that. In particular, languages with thousands to tens of thousands of glyphs, such as CJK (Chinese, Japanese, Korean), can have a single full-glyph font file reaching several MB, so take special care.

  • First ask whether you even need a web font. If the TV platform's built-in fonts are enough, not using a web font at all is the surest saving.
  • Load only the weights/styles you need. Limit to Regular/Bold and don't download unused Light, Thin, Italic, etc.
  • Use subset fonts. Building a font file that contains only the characters you actually use greatly reduces resident memory (the more glyphs a language has, the bigger the effect). unicode-range prevents downloading unused ranges when you split into multiple files, but to reduce resident memory itself, the fundamental approach is to prepare a subset file that contains only the glyphs you need.
  • Use font-display: swap so font loading doesn't block text rendering (better perceived performance). To reduce transfer size, use the well-compressed WOFF2.
/* Apply a subset (only the needed glyphs) to a specific character range */
@font-face {
  font-family: 'AppFont';
  src: url('app-latin-subset.woff2') format('woff2');
  unicode-range: U+0000-00FF; /* basic Latin only */
  font-display: swap;
}

4. DOM management and list/grid virtualization

After images, the biggest driver of a TV web app's memory is the number of DOM nodes. TV apps are especially vulnerable because they typically have long grid/carousel structures with hundreds to thousands of content cards.

4.1 Every DOM node is memory

  • Each DOM element carries a node object, computed style, layout info, and (if displayed) a render layer.
  • If you put thousands of cards into the DOM at once, all of them stay resident even when only a few are visible.
  • As the DOM grows, style recalculation, layout, and compositing costs grow too, hurting performance.

4.2 Virtualize long lists (windowing) — do it

Virtualization is a must for long lists/grids. Keep only the visible items (plus a small buffer) actually in the DOM, and recycle as the user scrolls.

Key ideas:

  • Render only the items visible in the viewport + a margin above and below, not the whole list.
  • Remove from the DOM or recycle elements that scroll out of view.
  • Fake the full scroll height with a spacer element or a transform.
// Conceptual example: simple windowing with IntersectionObserver
const BUFFER = 5; // extra items above and below

function renderWindow(startIndex, endIndex, allData, container) {
  container.textContent = ''; // remove previous items → reclaim DOM nodes & image memory
  const frag = document.createDocumentFragment();
  for (let i = startIndex - BUFFER; i <= endIndex + BUFFER; i++) {
    if (i < 0 || i >= allData.length) continue;
    frag.appendChild(createCard(allData[i]));
  }
  container.appendChild(frag);
}

In a TV app, any list with more than ~100 items should almost always consider virtualization. During validation, apps that "render thousands of cards as one big DOM" are a classic memory-overrun case.

4.3 Recycle elements — the DOM version of the object pool pattern

Same idea as the object pool (6.2). Creating and discarding a card every time increases DOM create/destroy and GC load. Instead, create a fixed number of DOM nodes and just swap their content.

// Create only as many card DOM nodes as can be visible, then reuse them
const pool = [];
function getCard() {
  return pool.pop() || createCard();
}
function recycle(card) {
  card.querySelector('img').removeAttribute('src'); // release image memory
  pool.push(card);
}

4.4 Clean up the previous screen's DOM on navigation

In an SPA, leaving the previous screen's DOM in place keeps occupying memory. On routing, remove the previous view's DOM and release its listeners, timers, and observers together. Giving each screen component a paired mount() and destroy(), where destroy() undoes everything it created (listeners, timers, observers, DOM), structurally prevents leaks. (See Section 5.)

4.5 Reduce excessive wrapper / shadow DOM

Unnecessary abstraction layers increase node count. Cut meaningless <div> nesting and excessive wrapper components, and remove unneeded layers to keep the DOM shallow and simple.

4.6 Beware compositing-layer (GPU memory) blowup

Separate from DOM node count, the browser promotes some elements to their own compositing layers, managed as GPU textures. Each layer takes width × height × 4 bytes of GPU memory, by the same principle as a decoded image. A single full-screen layer is about 8 MB (FHD), so if layers grow to dozens or hundreds, a TV with tight GPU memory hits its limit quickly.

Layers arise mainly from forced-promotion hints like will-change, transform: translateZ(0) / translate3d(0,0,0), or from animated elements.

  • Don't slap translateZ(0) on every card on the belief that "promoting to a layer makes it faster." It instead wastes GPU memory and increases compositing cost.
  • Turn will-change on only right before an animation and remove it when done. Leaving it on permanently keeps that many layers resident.
  • Don't stack multiple full-screen layers (front overlays, fade layers, etc.).
  • Use the DevTools Layers panel or the "Layer borders" option to check the actual layer count and sizes.

Rule: Promote layers "only when needed, only on the elements that need it." Don't sprinkle hints out of habit.


5. Preventing memory leaks

A memory leak is when an object that is no longer needed can't be reclaimed because something still references it, and it is fatal in long-running TV apps. The patterns that cause leaks in web apps are well known.

5.1 Unremoved event listeners (the most common cause)

A listener registered with addEventListener keeps holding the target object and the callback (and everything its closure captures) unless you explicitly remove it. In particular, listeners on long-lived objects like window and document stay until the app ends.

// Bad: the listener remains after the component is removed, holding the whole object graph
window.addEventListener('resize', this.onResize);

// Good 1: always remove in a matching pair
window.removeEventListener('resize', this.onResize);

// Good 2: clean up all at once with AbortController
const controller = new AbortController();
window.addEventListener('resize', onResize, { signal: controller.signal });
element.addEventListener('keydown', onKey, { signal: controller.signal });
// On cleanup:
controller.abort(); // removes every listener registered with this controller at once

Rule: Listeners you register when creating a component/screen must all be removed when destroying it. Making AbortController + signal your standard pattern reduces mistakes.

5.2 Reduce listeners with delegation

In a TV grid with hundreds to thousands of cards, attaching a click/keydown listener to each card keeps as many callback closures resident as there are listeners, plus register/unregister cost every time a card is created or discarded.

Instead, attach a single listener to a parent container and use event bubbling with event.target to determine which card fired it (event delegation). The listener count drops to one, cutting both memory and cleanup burden.

// Bad: a listener per card → N callback closures resident
cards.forEach(card => card.addEventListener('click', onClick));

// Good: delegate to a single container → 1 listener
grid.addEventListener('click', (e) => {
  const card = e.target.closest('.card');
  if (!card) return;
  handleSelect(card.dataset.id);
});
  • It pairs especially well with virtualization (4.2) and element recycling (4.3). Even when you swap card DOM, the container's listener stays, so you don't have to re-register a listener each time.
  • Cleanup is also just removing the one container listener, which reduces the listener-leak risk from 5.1.

5.3 Unremoved timers and callbacks

If setInterval, setTimeout, or requestAnimationFrame reference objects in their callbacks, those objects stay alive as long as the timer does. In particular, setInterval and a recursive requestAnimationFrame run forever unless you explicitly stop them.

const timerId = setInterval(update, 1000);
// Cleanup:
clearInterval(timerId);

let rafId = requestAnimationFrame(loop);
// Cleanup:
cancelAnimationFrame(rafId);

Always stop animation loops and polling timers when the screen goes to the background or is destroyed. You can detect visibility with the visibilitychange event.

5.4 Detached DOM nodes

A node removed from the DOM but still referenced by a JavaScript variable is not GC'd. This is called "detached DOM" and is a classic type of web app leak.

// Bad
const cache = {};
const list = document.getElementById('list');
cache.oldList = list;   // keeps a reference
list.remove();          // removed from the DOM
// Removed from the DOM, but cache.oldList still holds it, so it isn't reclaimed (including all child nodes)

// Good
cache.oldList = null;   // cut the reference so it can be reclaimed

5.5 Global caches/arrays that grow without bound

Logs, event records, response caches, and so on grow without limit if you keep piling them into global arrays/objects. Always cap the size (e.g., LRU) or clear periodically.

Because Map preserves insertion order, deleting and re-inserting an item on each access keeps the "least recently used" item at the front, giving you a simple LRU.

// A simple bounded LRU cache
const MAX = 50;
const cache = new Map();

function get(key) {
  if (!cache.has(key)) return undefined;
  const value = cache.get(key);
  cache.delete(key);     // delete, then
  cache.set(key, value); // re-insert → most-recently-used moves to the back
  return value;
}

function put(key, value) {
  if (cache.has(key)) cache.delete(key); // reset order on update
  else if (cache.size >= MAX) {
    cache.delete(cache.keys().next().value); // evict the least-recently-used (front)
  }
  cache.set(key, value);
}

5.6 Large objects held by closures

A closure captures variables from its outer scope. A single callback can keep a huge data structure alive. Reference only the values you actually need inside the callback.

5.7 Use WeakMap / WeakRef

When you want to attach extra info to an object without affecting its lifetime, use WeakMap. When the key object disappears, the related entry is reclaimed automatically.

const metadata = new WeakMap();
metadata.set(domNode, { lastFocused: 0 }); // when domNode is GC'd, this entry disappears too

5.8 Release memory aggressively when backgrounded

As seen in Section 1, when memory is low the TV kills background apps. If your app holds a lot of memory while off-screen (the user switched to another app/input), it becomes the first to be killed and reloads from scratch on return, hurting UX.

So the moment your app becomes invisible, give back memory you don't currently need.

  • Release offscreen images / decoded bitmaps (3.2)
  • Return videos/decoders not currently playing (7.1)
  • Clear caches you can rebuild (5.5)
  • Stop animation loops / polling timers (5.3)

Detect show/hide with visibilitychange (document.hidden). Release resources the moment the app is hidden, and restore only what's needed when it becomes visible again.

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    releaseOffscreenImages(); // release image bitmaps
    pauseAndReleaseVideos();  // return videos not currently playing
    stopAnimationLoops();     // stop rAF / timers
    trimCaches();             // clear caches you can rebuild
  } else {
    restoreOnResume();        // on return, re-prepare only what's needed
  }
});

Rule: Preserve lightweight state such as scroll position and focus, but drop heavy resources (bitmaps, buffers, caches) in the background and rebuild them on return.

5.9 How to diagnose leaks

  • In Chrome DevTools' Memory panel, take two or more heap snapshots and compare them (Comparison) to find objects that keep growing.
  • Filter by "Detached" to directly inspect detached DOM nodes.
  • Open and close the same screen several times, then compare snapshots; objects that keep accumulating even after closing indicate a leak.

6. JavaScript memory and GC management

6.1 Suppress memory churn

Memory churn means creating and discarding many temporary objects in a short time. This makes GC run often, leading to animation frame drops and performance degradation.

Watch these places especially:

  • Inside animation loops (requestAnimationFrame)
  • Inside scroll handlers and input (remote-control key) handlers
  • Inside frequently running for loops
// Bad: a new object/array every frame
function onFrame() {
  const pos = { x: computeX(), y: computeY() }; // new object every frame → GC pressure
  moveTo(pos);
  requestAnimationFrame(onFrame);
}

// Good: reuse the object
const pos = { x: 0, y: 0 };
function onFrame() {
  pos.x = computeX();
  pos.y = computeY();
  moveTo(pos);
  requestAnimationFrame(onFrame);
}

To maintain 60 fps, the budget per frame is about 16 ms. To stay within it, minimize allocations during the frame.

6.2 Object pool — use with care

Returning reusable objects (particles, card view-models, etc.) to a pool instead of discarding them can reduce allocation/GC. But a pool has its own management cost, and an oversized pool actually increases resident memory and GC load. Apply it only where allocation is a real bottleneck (hot paths), after measuring.

6.3 Choose efficient data structures

Choosing the right data structure for the job saves memory.

  • For large numeric data, use a TypedArray (Int32Array, Float32Array, Uint8Array, etc.) instead of a plain array ([]). It stores the values themselves densely in contiguous memory without creating a separate object per element, so the difference is large compared with, for example, an array of {x, y} objects.
  • For frequent key-value lookups, Map is more suitable and clearer than an object ({}); for membership only, use Set.
  • When handling large arrays, don't overuse unnecessary copies (slice, spread [...arr], concat). Each copy allocates new memory.
// Bad: a million coordinates as an array of objects
const points = [];
for (let i = 0; i < 1_000_000; i++) points.push({ x: 0, y: 0 });

// Good: a single TypedArray (x, y interleaved)
const points = new Float32Array(1_000_000 * 2);

6.4 Watch strings

  • When concatenating string fragments many times, collecting them in an array and joining once with join() is safer for memory and performance.
  • Don't keep a huge response string in its original form for long. Extract only what you need, then cut the reference to the original so it can be reclaimed.

6.5 Cut references explicitly

Assign null to a large object you no longer need to cut the reference so GC can reclaim it (especially when the variable's scope is wide or long-lived).

this.bigData = null; // release the large data

Note: To empty a value, obj.prop = null is generally better than delete obj.prop. delete can break the engine's internal object optimizations and hurt performance.

6.6 Your app code (bundle) takes memory too

So far we've covered data created at runtime (images, DOM, objects), but your app's JavaScript/CSS code itself uses memory. V8 parses loaded scripts and keeps them resident, so the bigger the bundle, the higher the baseline footprint from startup. The less code you load, the less resident memory.

  • Don't include unnecessarily large libraries. Instead of pulling in a heavy dependency whole for one or two features, use only the parts you need, or consider a lighter alternative or your own implementation. Also avoid including duplicate libraries that do the same thing.
  • minify + tree-shaking at build time to remove unused (dead) code.
  • Code-split to load only per-screen code. Don't ship every route's code on the first screen; split with dynamic import (import()) at entry time. Not loading unused screens' code into memory at all is best.
  • Excessive abstraction layers increase code size. The same principle as 4.5 (removing unnecessary DOM wrappers) applies to code.

7. Video & media memory management

The core of a TV app is mostly video playback. Media uses a lot of memory, so managing it matters.

7.1 Always release media resources that finished/left playback

If you only remove a <video> element from the screen and leave it, the decoder and buffers may remain. Release it definitively in this order.

function releaseVideo(video) {
  video.pause();
  video.removeAttribute('src');   // remove the src attribute
  // If you used <source> child elements, remove those too.
  video.load();                   // prompt release of internal buffers/decoder
  // If you used MSE, also clean up SourceBuffer/MediaSource
}
  • Don't keep multiple <video> elements alive at once. When implementing preview autoplay etc., release a preview video the moment it leaves the screen. A TV has a limited number of hardware video decoders, and each decoder uses large buffers.
  • Reusing a single <video> element is safer than creating and discarding one per content item.

7.2 MSE (Media Source Extensions) buffer management

  • If you implement adaptive streaming yourself, remove the buffer for already-played ranges with SourceBuffer.remove(). Otherwise the buffer keeps growing and eats memory.
  • Cap the forward buffer. Prefetching minutes ahead is convenient but uses a lot of memory. Be conservative with buffer size, especially at 4K / high bitrate.

7.3 Subtitle / thumbnail tracks

  • Don't keep thumbnail-preview (scrub-preview) sprites fully resident; load/release only the ranges you need.
  • Reclaim used subtitle tracks and blob URLs with URL.revokeObjectURL() (a blob URL is not released unless you explicitly revoke it).
const url = URL.createObjectURL(blob);
// After use:
URL.revokeObjectURL(url);

8. Data handling optimization (fetch / JSON / cache)

The amount and handling of data fetched over the network directly affect memory usage.

8.1 Fetch only as much as you need (server-side filtering & paging)

  • Don't fetch thousands of list items at once; split them via paging / infinite scroll.
  • Don't receive large responses containing fields the client won't use. Design the API so the server filters down to only the needed fields (e.g., GraphQL field selection, a REST fields= parameter).
  • Fetching, parsing, and storing large data itself leads to excessive memory use, which in turn degrades performance.

8.2 Understand the cost of JSON.parse

  • JSON.parse(str) turns a string into an object tree, taking memory for the parsed objects on top of the original string. The larger the JSON, the more this double occupancy hurts.
  • Don't keep holding the original string reference after parsing (cut it to allow reclamation).
  • For very large datasets, consider streaming parsing or partial parsing. Avoid the pattern of parsing everything and holding it all.

8.3 Don't hold parse results whole for long

  • Rather than keeping the entire list API response in memory, transform it into the shape the screen needs (a view-model) and then discard the original response.
  • In infinite scroll, cap and clean up data for pages already passed (far from the screen). Data, like the DOM, is a target for "windowing."

8.4 Efficient data formats

For large, repetitive data, use a compact representation rather than a verbose format. JSON is usually the standard on the web, but consider the following.

  • Avoid unnecessarily nested or verbose JSON structures; keep key names and structure compact.
  • Don't stuff binary data into JSON as base64 (about 33% bloat); receive it as a separate binary response or an ArrayBuffer.
  • If very large structured data is sent repeatedly, consider binary serialization (e.g., protobuf, CBOR).

8.5 Cap your caches

  • Cap response caches, image caches, and computed-result caches with an LRU limit so they don't grow without bound (see 5.5).
  • Don't put large data in localStorage/sessionStorage; they have size limits and are synchronous APIs (bad for performance). If you truly need large data, use IndexedDB — and still manage a cap.

9. Canvas / WebGL / Web Audio / Worker cautions

More powerful web APIs can use a lot of memory and often hold resources GC won't reclaim automatically. The following applies only if you use these APIs directly — skip it if you don't.

9.1 Canvas 2D

  • Canvas back-buffer memory = width × height × 4 bytes. Don't create many large canvases.
  • For an unused canvas, shrink its size to width = height = 0 to reclaim the back buffer, and cut the reference.
  • Creating a large array every frame with getImageData/putImageData causes memory churn. Reuse buffers.

9.2 WebGL

  • WebGL resources (textures, buffers, programs, framebuffers) are not reclaimed by GC automatically. Delete them explicitly: deleteTexture, deleteBuffer, deleteProgram, deleteFramebuffer.
  • When you no longer need the context, release it with getExtension('WEBGL_lose_context').loseContext().
  • Textures use large GPU memory by the same principle as image bitmaps. Control texture size and count.

9.3 Web Audio

  • Close an AudioContext with close() after use. Don't leave many open.
  • A decoded AudioBuffer can be large; cut the reference when done.

9.4 Web Worker

  • A Worker has its own memory space. Terminate a finished Worker with terminate(); leaving it keeps occupying memory.
  • Passing large data to a Worker copies it (structured clone), creating two copies. Use a Transferable (e.g., transferring ownership of an ArrayBuffer) to avoid the copy.
worker.postMessage(buffer, [buffer]); // transfer ownership of buffer → no copy

9.5 iframe

  • Each iframe loads its own document and resources, using a lot of memory. Create ad / external-widget iframes only when needed, and remove them from the DOM to destroy them completely when done.

In closing

TV web app memory optimization boils down to three principles:

  1. Create only as much as you show — images at display size, lists only as far as visible, data only as much as needed.
  2. Release the moment it's no longer needed — clean up listeners, timers, observers, media, DOM, and caches along their lifecycle.
  3. Measure and verify on a real device — confirm memory doesn't trend upward across repeated open/close and long runs.

Apply these three principles from the design stage, and you'll deliver a smooth, stable app experience to users.


This document is a draft and will be continually improved through review. Refer to separate platform documentation for platform-specific policies and support details.

↑ Back to top