Award Winning Company

Award Winning Company

DIAGNAL Logo

Optimising Lightning.js WebTV Applications for Smart TVs

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:

  1. Memory is allocated
  2. The object is used
  3. References are removed
  4. 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.

Memory Reduction over time
Memory Reduction over time
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.

Shader continuous repainting
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 from lightningjs-ui into 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 using requestAnimationFrame. 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)
MetricBefore optimizationAfter optimizationImpact
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 timeBlocked by full list rendering (forceLoad)Progressive and viewport-based rendering~35–50% faster first meaningful paint
Active rendered itemsEntire dataset renderedOnly viewport-visible items rendered~60–75% reduction in active UI nodes
GPU texture retentionIncreasing due to unreleased texturesControlled lifecycle cleanupSignificant GPU memory stabilization
FPS on low-end devicesNoticeable drops during scrollingStable 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.