This document outlines the root causes and solutions for the issues encountered while running the Solid TV demo application on the LG WebOS TV (LGWhite device, model 24LM520S-WU, WebOS SDK 3.8.0).
Older Smart TV browsers (such as Chromium 38/53 on webOS 3.x) do not natively support ES6 Proxy features. When Proxy is not present, solid-js (v1.9.9) runs a fallback path for merging properties (mergeProps).
This fallback path executes the following routine:
- It retrieves all properties of the source object (including getters and function methods) via
Object.getOwnPropertyNames(source). - When the source is a SolidJS memo (which is under the hood a strict-mode function) or an
argumentsobject,Object.getOwnPropertyNamesreturns internal strict-mode properties:'caller','callee', and'arguments'. - The fallback path then calls
Object.getOwnPropertyDescriptor(source, key)for each property. - Attempting to query descriptors for
'caller','callee', or'arguments'on strict-mode functions or arguments objects immediately throws:TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
This unhandled promise rejection crashed the router's location state construction on startup, preventing the application's view components from rendering.
We globally intercepted Object.getOwnPropertyNames at the start of the app entry point in src/index.tsx:
// Patch Object.getOwnPropertyNames to prevent strict mode errors in solid-js mergeProps
(function() {
const originalGetOwnPropertyNames = Object.getOwnPropertyNames;
Object.getOwnPropertyNames = function(obj) {
const names = originalGetOwnPropertyNames(obj);
if (obj && (typeof obj === 'function' || Object.prototype.toString.call(obj) === '[object Arguments]')) {
return names.filter(function(name) {
return name !== 'caller' && name !== 'callee' && name !== 'arguments';
});
}
return names;
};
})();This safely filters out forbidden strict-mode keys before solid-js tries to query their descriptors.
After fixing the JavaScript runtime crash, the application successfully loaded the route and queried metadata from the TMDB API (https://api.themoviedb.org). However, all requests to load poster and backdrop assets from the secure TMDB image server (https://image.tmdb.org/...) failed with:
net::ERR_INSECURE_RESPONSE
Older Smart TVs lack updated root certificate stores (e.g. they fail to recognize newer Let's Encrypt or Cloudflare TLS certificates / chains used by modern content delivery networks), causing the TV's browser engine to reject the SSL handshakes.
We modified the TMDB API configuration module in src/api/index.ts to retrieve images using the non-SSL endpoint (base_url instead of secure_base_url):
function loadConfig() {
return _get("/configuration").then((data) => {
tmdbConfig = data;
- baseImageUrl = data.images?.secure_base_url;
+ // Fallback to HTTP base_url to prevent net::ERR_INSECURE_RESPONSE on older TV browsers
+ baseImageUrl = data.images?.base_url || data.images?.secure_base_url;
return data;
});
}By requesting assets via http://image.tmdb.org/t/p/..., we bypassed the SSL validation check, allowing images to load and render successfully.
In vite.config.js, the Babel legacy compiler was configured with the following assumption:
constantSuper: trueThis optimization instructs Babel to drop _get / _superPropGet helper methods on ES6 class super calls. Under ES5 targets, this assumption incorrectly compiles the TypeScript statement super.color = value (inside @solidtv/renderer's CoreTextNode) to this.color = value.
Because this statement runs inside CoreTextNode.prototype.color's setter, calling this.color = value caused the setter to invoke itself recursively, throwing:
Uncaught RangeError: Maximum call stack size exceeded
This stack overflow was triggered whenever a text node's color property was set, which happens extensively when rendering the list of Demo Tiles on the Examples page, crashing the application.
We configured constantSuper: false in vite.config.js:
assumptions: {
setPublicClassFields: true,
privateFieldsAsProperties: true,
- constantSuper: true,
+ constantSuper: false, // ensures super.color calls compile to _superPropGet helpers instead of recursive assignments
noClassCalls: true,
noDocumentAll: true
}This instructs Babel to compile super.color = value correctly to ES5 helper functions that lookup the parent's setter prototype, resolving the crash at build time without requiring any runtime patches in the codebase.
Older Smart TV browsers (like Chrome 38/53 on webOS 3.x) do not support createImageBitmap, which forces the renderer to perform image decoding synchronously on the main thread during texture upload (texImage2D).
If the framework attempts to spawn image Web Workers (the default configuration is 4 workers), these workers remain idle/non-functional because they cannot access modern image decode mechanisms in the worker context, consuming unnecessary overhead. Additionally, bursts of concurrent image requests block the UI thread, causing massive frame drop stutters.
We configured dynamic main-thread image loading optimizations in src/index.tsx:
- Disable Workers: Set
numImageWorkers = 0dynamically ifwindow.createImageBitmapis unsupported, avoiding worker instantiation overhead. - Limit Concurrency: Capped
imageDecodeConcurrencyat2(down from4) on legacy browsers to pace main-thread image decodes and keep the render loop active.
When porting or testing the application on other smart TV platforms (e.g. webOS 4+, Tizen, Playstation, etc.), perform the following checks:
- Check: Does the client browser engine natively support ES6
Proxy? (Check ifwindow.Proxyis defined). - Fix: If unsupported, ensure the global
Object.getOwnPropertyNamespatch in src/index.tsx is active. This prevents strict-mode errors in SolidJS'smergePropsfallback route copy routines.
- Check: Do HTTPS image URLs (e.g. from Cloudflare, modern CDNs) fail to load with certificate handshaking errors?
- Fix: Force non-SSL endpoints (HTTP) for media assets in src/api/index.ts.
- Check: Does navigating to pages with multiple text elements (e.g. Examples, Lists) cause
RangeError: Maximum call stack size exceededinCoreTextNode? - Fix: Ensure that
constantSuperis set tofalseinvite.config.js(vite.config.js). This ensures Babel compiles classsupersetter delegates correctly for ES5 targets.
- Check: Does pressing the Left arrow button on the remote move focus to the right or fail to trigger parent/drawer navigation in
VirtualGrid? - Fix: Check
e.keystrings returned by the browser. If the browser returns legacy names (e.g."Left"instead of"ArrowLeft"), the global event normalizer in src/index.tsx must be enabled.
- Check: Do automated key simulation loops fail to navigate?
- Fix: Check if
new KeyboardEventconstructor throws anIllegal constructorerror. If so, replace withdocument.createEvent("Event")andObject.definePropertyin src/pages/Benchmark.tsx.
- Check: Does the app experience major frame rate stutters when loading rows of images?
- Fix: Check if
createImageBitmapis supported. If false, disable image workers and setimageDecodeConcurrencyto2to throttle main-thread rendering loop blockages.
- vite.config.js — Disabled
constantSupercompiler assumption. - src/index.tsx — Added strict-mode property filter patch, legacy event key normalizer, and main-thread image loading optimizations.
- src/api/index.ts — Dynamic HTTP base URL fallback configuration.
- src/api/tmdbData.ts — Promise logs wrapper for API transparency.
- src/api/providers/browse.ts — Promise catch blocks to prevent uncaught rejections.
- src/pages/Benchmark.tsx — Replaced standard constructor key simulation with backwards-compatible initEvent.
In src/pages/Benchmark.tsx, automated key navigation for the benchmarking routine was implemented via the standard ES6 constructor:
new KeyboardEvent("keydown", { ... })Older browser engines (like Chrome 38) do not support the KeyboardEvent constructor, throwing an Illegal constructor type error or creating a non-functional event instance where key properties are read-only and/or cannot be set. This caused the benchmark automation to fail silently without navigating.
We replaced new KeyboardEvent(...) with the widely supported DOM Level 3 initialization method, followed by Object.defineProperty to configure the event properties on the dispatched event target:
function simulateKeyDown(key: string) {
try {
const event = document.createEvent("Event");
event.initEvent("keydown", true, true);
Object.defineProperty(event, "key", { value: key, enumerable: true, configurable: true });
Object.defineProperty(event, "code", { value: key === "ArrowDown" ? "ArrowDown" : "ArrowUp", enumerable: true, configurable: true });
document.dispatchEvent(event);
} catch (e) {
console.error("Failed to simulate key down:", e);
}
}This ensures key simulations are successfully generated and dispatched on both legacy Smart TVs and modern desktop environments.
Under @solidtv/solid/primitives (specifically the navigation helper navigableHandleNavigation), grid selection movement is resolved by checking:
export const navigableHandleNavigation = function (e) {
return moveSelection(this, e.key === 'ArrowUp' || e.key === 'ArrowLeft' ? -1 : 1);
};Older browser engines (like Chromium 38/53 on webOS 3.x) return legacy string values for directional keys, where the Left arrow key is identified as "Left", NOT "ArrowLeft".
Because the check e.key === 'ArrowLeft' returned false, the helper defaulted to 1 (which signifies moving the selection to the Right). As a result, pressing the Left button on the TV remote moved focus to the right.
We registered a global capturing event listener on window level inside src/index.tsx to intercept all key events early and normalize legacy key names (Left, Right, Up, Down) to their standard Arrow equivalents:
// Normalize legacy arrow keys to standardized values for third-party navigation libraries
(function() {
const keyMap = {
'Left': 'ArrowLeft',
'Right': 'ArrowRight',
'Up': 'ArrowUp',
'Down': 'ArrowDown'
};
function normalizeKey(e) {
if (e && keyMap[e.key]) {
try {
Object.defineProperty(e, 'key', {
value: keyMap[e.key],
configurable: true,
writable: true,
enumerable: true
});
} catch (err) {}
}
}
window.addEventListener('keydown', normalizeKey, true);
window.addEventListener('keyup', normalizeKey, true);
})();This intercepts events before the framework or VirtualGrid listeners receive them, ensuring legacy browsers always present standard key strings, correcting the left direction navigation.
- On LG webOS TVs, pressing the BACK button on the remote control dispatches a
KeyboardEventwith key string"Back"or"GoBack"andkeyCode461. - In
src/pages/App.tsx,useFocusManager(...)was invoked with a customuserKeyMapthat definedBack: ["b"]. BecauseuseFocusManagerflattens its arguments into the framework's global key map, this wiped outConfig.keyMap.Backconfiguration fromsrc/index.tsx, overridingBackto ONLY accept the'b'key. - Consequently, when key code
461or string"Back"arrived from the remote control,@solidtv/solidlooked upkeyMapEntries["Back"]andkeyMapEntries[461], receivedundefined, and failed to triggeronBackhandlers (<view onBack={() => navigate(-1)} />).
- Updated
useFocusManagerinsrc/pages/App.tsxto include all back button keys:
Back: ["Back", "GoBack", "b", 461, 10009, "Escape", 27]- Added
'GoBack': 'Back'to the global capture-phase event normalizer insrc/index.tsx. - Updated
Config.keyMap.Backinsrc/index.tsxto include"Back","GoBack", and461.