Developing applications for Smart TVs differs fundamentally from building for web or mobile platforms. TV devices operate under strict constraints: limited CPU power, shared GPU memory, reduced RAM, and embedded browser engines that behave differently from desktop Chrome or mobile Safari.
At Diagnal, we build and operate OTT platforms across various Smart TVs and set-top boxes. While working on our Lightning.js-based WebTV application, we faced performance challenges that appeared only after extended use and real-device testing. These included memory leaks, unstable rendering, degraded scrolling, slow homepage loads, and occasional crashes on low-end devices like older LG WebOS TVs.
This article documents our optimization process, from understanding performance fundamentals in embedded environments to identifying bottlenecks with developer tools and implementing custom rendering, memory management, and data-loading strategies in production.
What is Lightning.js?
Lightning.js is a JavaScript UI framework designed for embedded platforms such as Smart TVs and set-top boxes.
Unlike traditional frontend frameworks, Lightning does not use HTML or CSS. It relies on a custom templating system and renders UI using WebGL. This approach leverages the GPU directly for drawing UI elements, enabling smooth animations and transitions on constrained hardware when built with performance awareness.
Lightning provides essential TV application features like focus management, remote navigation, and predictable rendering pipelines. These design choices make Lightning suitable for TV development but require developers to manage memory and rendering behavior typically handled automatically in DOM-based applications.

Understanding performance in TV applications
Before optimizing, understand what performance means in a TV environment.
Memory usage
TV devices have limited RAM. If memory is not released properly, applications slow down and eventually crash.
Frames per second (FPS)
FPS measures interface rendering smoothness. Low FPS causes laggy scrolling, delayed navigation, and poor visuals.
Page load time
This is the time to display meaningful content, especially after launch. Long load times reduce user engagement.
Understanding memory leaks
A memory leak occurs when allocated memory is not released after use.
In embedded environments, a memory leak is:
A situation where system or graphics memory grows at a rate that negatively impacts user experience.
Not all leaks are immediately problematic. A slow leak growing a few megabytes over hours may not affect typical sessions. Fast-growing leaks or those blocking garbage collection require urgent fixes.
Avoid over-optimizing based on unrealistic stress tests. Rapidly opening and closing screens without idle time can prevent garbage collection and create misleading results. Our testing evolved to reflect real user behavior, including navigation pauses and idle periods.
Types of memory leaks in Lightning applications
We focused on three main categories.
JavaScript memory leaks
These occur when JavaScript objects remain referenced and cannot be garbage-collected.
Common causes include:
- Unremoved event listeners
- Forgotten intervals and timeouts
- Global variables
- Retained closures
- Large network responses
These leaks cause continuous growth of the JS heap.
Graphics (GPU) memory leaks
Lightning renders UI through WebGL. Every image, font, and visual element becomes a GPU texture.
If textures are not released, GPU memory fills up, leading to:
- Missing posters
- Black areas
- Rendering failures
Browser-level memory issues
Rarely, memory problems originate in the embedded browser. These platform-specific issues require collaboration with browser integration teams.
How JavaScript manages memory
JavaScript uses automatic garbage collection, which works only when objects are no longer referenced.
Memory follows a simple lifecycle:
- Memory is allocated
- The object is used
- References are removed
- Garbage collection reclaims memory
Developers control only the first three steps. If any reference remains, such as an event listener or closure, garbage collection cannot reclaim memory.
This understanding was critical in diagnosing our leaks.
Using developer tools for performance analysis
We relied heavily on Chrome Developer Tools for profiling.
Primary tools
- Memory Profiler
- Heap Snapshots
- Performance Timeline
- FPS Meter
- Paint Flashing
- Chrome Task Manager
Heap snapshot analysis
Using DevTools → Memory tab, we captured snapshots at different stages:
- After launch
- After navigation
- After extended usage
We compared snapshots to identify:
- Retained objects
- Growing closures
- Detached components
This comparison helped isolate memory leaks.

Multi-level memory observation
We monitored:
- Total browser memory
- Web process memory
- JavaScript heap
JS heap growth was the most reliable indicator of application-level leaks.
Fixing memory leaks through lifecycle management
Most JavaScript leaks in our application originated from unmanaged event listeners and references.
We adopted strict lifecycle discipline: every subscription must have a cleanup path.
We introduced centralized cleanup logic by tracking cleanup callbacks instead of scattered removeListener calls.
private _eventListeners: Array<() => void> = [];
override _active(): void {
this.application.on('appLanguageChanged', this._appLanguageChanged);
this._eventListeners.push(() => {
this.application.removeListener(
'appLanguageChanged',
this._appLanguageChanged
);
});
}
override _inactive(): void {
this._eventListeners.forEach(cleanup => cleanup());
this._eventListeners = [];
}All listeners paired with cleanup handlers were removed during deactivation, allowing garbage collection to reclaim memory.
Optimizing images, textures, and shaders
Our application was image-driven, placing significant pressure on GPU memory.
We optimized rendering by:
- Avoiding image reloads when the source did not change
- Explicitly clearing unused textures
- Destroying off-screen components
- Removing unnecessary visual effects
For example, images updated only if the source changed:
if (this.icon.src !== newSrc) {
this.icon.src = newSrc;
}We identified shaders causing continuous repainting. Using paint flashing, we found Lightning.shaders.Vignette triggered unnecessary redraws across multiple screens. Removing this shader stabilized rendering, especially on low-end devices where the app previously crashed.
Identifying rendering and repainting issues
Using DevTools → Rendering panel, we enabled:
- FPS Meter
- Paint Flashing
Paint flashing highlighted components repainting every frame without user interaction. This helped eliminate redundant animations, shader side effects, and layout recalculations that degraded performance.
Reducing unnecessary repainting improved FPS and GPU memory stability.
Improving homepage loading performance
Initially, our homepage used forceLoad: true, loading all list items before rendering. This caused long startup times and memory spikes.
Lazy loading and destruction helped but did not fully solve the problem. We needed finer control over rendering.
- Building a custom optimized list system
We copied the list component fromlightningjs-uiinto our codebase and customized it extensively.This enabled:- Viewport-based rendering
- Progressive and chunk-based loading
- Frame-budgeted creation and destruction
- Predictive loading based on navigation
- Viewport-based rendering
Only items visible on screen were rendered. For a 1920px layout, this meant rendering roughly eight portrait or six landscape cards at a time. Other items remained as data until needed.This reduced active memory usage and improved scrolling smoothness. - Progressive and chunk-based loading
Large datasets were split into chunks. Initial items rendered immediately for quick display, while remaining items loaded incrementally with small delays.This ensured fast first paint without blocking the main thread.
setTimeout(() => {
this._processChunkedItems();
}, 16);- Asynchronous creation and destruction
Creation and destruction were queued and processed usingrequestAnimationFrame. This distributed heavy operations across frames, preventing UI freezes and preserving responsiveness. - Predictive loading during navigation
Adjacent items were preloaded based on focus movement.
lazyLoadData(nextIndex);
lazyDestroyData(nextIndex);This minimized perceived loading delays during scrolling.
Managing garbage collection and memory stability
Lightning provides manual garbage collection via this.stage.gc(), but triggering it too often can hurt performance.
We treated garbage collection as a controlled operation, triggering it only at safe lifecycle boundaries and tuning thresholds based on the lowest-end devices supported.
Memory pressure settings ensured garbage collection occurred before devices reached critical limits.
this.stage.gc();Embedded testing and validation strategy
We avoided unrealistic stress tests and focused on:
- Realistic navigation patterns
- Idle periods
- Long-session testing
- Platform-specific validation
This ensured reliable performance measurements.
Common memory and performance pitfalls
Throughout optimization, we eliminated:
- Unreleased listeners
- Forgotten timers
- Retained screen instances
- Oversized images
- Duplicate textures
- Excessive off-screen rendering
These patterns caused most long-term stability issues.
Key engineering learnings
This journey reinforced several principles:
- Profiling must precede optimization
- Lifecycle management is fundamental
- GPU memory is as important as JS memory
- Rendering should match visibility
- Custom infrastructure enables scalability
Performance summary (before vs after optimization)
| Metric | Before optimization | After optimization | Impact |
|---|---|---|---|
| JS heap after navigation | ~72 MB and continuously growing | ~64–65 MB stabilized | ~11% reduction and eliminated progressive growth |
| Heap growth per navigation cycle | +3–5 MB per cycle | <1 MB per cycle | ~70–80% reduction in leak rate |
| Homepage initial load time | Blocked by full list rendering (forceLoad) | Progressive and viewport-based rendering | ~35–50% faster first meaningful paint |
| Active rendered items | Entire dataset rendered | Only viewport-visible items rendered | ~60–75% reduction in active UI nodes |
| GPU texture retention | Increasing due to unreleased textures | Controlled lifecycle cleanup | Significant GPU memory stabilization |
| FPS on low-end devices | Noticeable drops during scrolling | Stable scrolling performance | ~25–40% smoother frame stability |
Conclusion
Optimizing Lightning.js applications for Smart TVs requires more than isolated fixes. It demands deep understanding of embedded browsers, JavaScript memory behavior, GPU pipelines, and real user interaction patterns.
By combining systematic profiling, disciplined lifecycle management, custom list virtualization, and progressive rendering strategies, we built a stable and scalable WebTV application across diverse devices.
At Diagnal, performance is a core engineering responsibility, not a post-launch concern. Lessons from this journey continue to influence how we design, build, and optimize TV experiences across platforms.







