Handling performance issues in Smart TV applications
Tags: javascript, smarttv, performance, webdev
Published: 2023-02-06 09:50:36

If you have ever developed applications for Smart TVs, you must know what it's like to write code with high performance and backward compatibility in mind. However, there are circumstances in which you might not be able to tackle performance issues with conventional tools - inheriting an old codebase, or dealing with slow libraries or API response times are just an example!

In contrast to 'normal' devices, Smart TVs require a special attitude when developing apps as you are guaranteed to work with a low-end CPU that runs an older version of Chromium. For instance, WebOS 3.0 made in 2016 retains a whopping **38** Chromium version!!!

This post will cover tools and coding techniques that will help boost your Smart TV application performance, regardless of the framework you use. Let's jump right in:


### 🫧 CSS Transitions and Animations 

Good animation performance means reaching the ultimate goal of 60 frames per second. When it comes to Smart TVs, there are reasons basic animations sometimes look like they are glitchy or don't start moving when required. When dealing with such an issue, it is important to understand the browser rendering process.

This small diagram shows the browser rendering sequence:

![Browser rendering sequence diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u1sk5k3zm97pnn08ds53.png)

Layout and Paint are the most expensive operations, not only because they have to trigger the operations that follow to complete the render cycle, but because they can force the browser to re-render other elements. For instance, changing CSS properties like `width` or `margin` on a parent element affects the size of child elements. As a result, re-render goes through the waterfall for each child. This is why animating CSS properties that trigger Layout or Paint may be bad for performance — it simply creates a lot of work for the browser which is a strain on the CPU. [CSS Triggers](https://csstriggers.com/) is a useful link that shows what rendering step is triggered by every CSS property.

As a result, achieving better animation performance means animating properties that only trigger the last step of the rendering process - _Composite Layers_. This way, you jump over most of the rendering process and unload the main thread. Two properties do this — **transform and opacity**. Speaking of the main thread, animating elements via Javascript (e.g. [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame)) is rarely a good choice, as it _always_ runs on the main thread.

Another useful CSS property is [will-change](https://developer.mozilla.org/en-US/docs/Web/CSS/will-change) - by using it you are explicitly asking the browser to save resources in order to use them to animate the element. However, it is advised to use this exclusively on transitions that are expected to be a performance bottleneck.


### 🎨 Layout thrashing

Layout thrashing is the second most common performance issue on Smart TVs, which is also related to the browser rendering process. Overall, Smart TV applications need to be mindful of when they measure and invalidate layout. When tackling this problem, your thinking should be 'How do I avoid layout recalculation?'. [CSS Triggers](https://csstriggers.com/) and [this amazing gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) will be of great help.


### 🖖 Code-splitting and lazy-loading Javascript

Component [lazy-loading](https://webpack.js.org/guides/lazy-loading/) is essential if you care about page loading times. The little trade-off that comes into play is that consequent pages might load a little slower. Nevertheless, you will make sure that your users will never download unnecessary code and your browser will do less parsing work.

Your framework of choice may provide its way of lazy loading components but generally speaking, you can call it an asynchronous import.

Let us say we have a module with a component:

```javascript
// myComponent.js
const component = () => {
 const div = document.createElement('div');
 div.innerText = 'My component';
 return div;
}

const myComponent = component();

export default myComponent;
```

```javascript
// index.js
/* this will asynchronously load the component module with a
pre-defined chunk name */

import(/* webpackChunkName: "myComponent" */ './myComponent')
 .then(module => {
   const component = module.default;
   document.body.appendChild(component);
})
```

This way you will make sure, loading the component script file is deferred until your component is needed.


### 📜 Module scripts vs classic scripts.

To unblock the main thread, you can modularize scripts or use `async` of `defer` attributes to tell the browser they can be processed later

```html
<script src="index.js" defer /> 
<!--this will load and execute after parsing the document, but 
before all the content has been loaded-->

<script src="index.js" async /> 
<!--this will load and execute as soon as possible regardless
of whether or not the content has been loaded-->

<script type="module" src="index.js" /> 
<!--this will be deferred by default-->
```

> Be careful with `<script type="module" />` on Tizen devices, as you cannot serve module scripts from the filesystem - you will get: 
`Failed to load module script: The server responded with a non-JavaScript MIME type of "". Strict MIME type checking is enforced for module scripts per HTML spec.`

If you would like to learn more, I recommend [this article](https://zellwk.com/blog/javascript-async-and-defer/).


### 🧺 Static assets: images, fonts, and video

#### Fonts
Fonts are tricky, as better performance might affect how your app looks. Nevertheless, you can experiment with it:

- Implement [Font preloading](https://web.dev/codelab-preload-web-fonts/), like so:

```html
<link 
 rel="preload" 
 href="/yourFontDirectory/your-font.woff2" 
 as="font" 
 type="font/woff2" 
 crossorigin
> 
```

- [Swapping out fonts on load](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display) will help smooth out the substitution effect.

- Implement a font caching strategy. Keep in mind that browser caching can only be used when the fonts you serve are from your domain or CDN – you cannot cache fonts served from 3rd parties, (e.g. Google Fonts).

#### Images
Images are even trickier, as in this case you have to move all the image work to the server. 

- Make sure the image you download will be the one you are going to use - **don't resize images in the browser!** 
- Don't [decode images](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/decode) in the browser! Smart TVs have notoriously small decoders.
- Don't download images if you don't have to! use `loading="lazy"` or `IntersectionObserver` (there is a polyfill for it).
- Serve images in the best possible format. These days, `webp` is recommended, as it helps preserve the image quality with 25% less size.

#### Video
Video is the most complex media element that is rendered on the page, so you have to be extra careful with this one!

- **Don't use `autoplay` on video** - it will tell the browser to start downloading the video immediately. Play the video once you make sure most of the work related to playback is done on the main thread. Depending on the initial size of your video, it might start the playback with a big stutter if autoplay is on. You can use `canplay` or `canplaythrough` to initiate playback.
- Do not [preload video](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-preload) if you are not going to play it - make sure you don't use CPU resources if you don't expect the user to play the video.
- The biggest bottleneck for any video on a Smart TV is video chunk size - playing a big-sized asset will require a lot of work from the CPU. Make sure your chunks are under 200 Kb so that video playback achieves top smoothness on any device.


### ⛓️ The `<link>` tag

The `<link>` tag was briefly mentioned in this article before, but this tag can come in handy in a few more use cases. There are 6 tags that handle preloading - `preload`, `preconnect`, `dns-prefetch`, `prefetch`, `prerender` and `modulepreload`. We will focus on three of them, that, in my view, have a significant impact on performance.

#### Preload

`<link rel="preload">` instructs the browser to download and cache a resource as soon as possible according to the potential destination given by the `as` attribute (and the priority associated with the corresponding destination). It is helpful when you need that resource sometime after the page is loaded. The browser doesn’t do anything with the resource after downloading it. It is just cached to be used later on.

#### Preconnect

`<link rel="preconnect">` instructs the browser to perform a connection to a domain in advance. Preconnecting to needed domains speeds up future loads from the domains you specify by establishing a connection between a server and a site in advance. This is useful when you know you are going to make lots of API requests to domains other than your origin.

#### Dns-prefetch

`<link rel="preconnect">` works more or less the same way as `preconnect`, though it does less work (only DNS resolution). You would want to use it more than `preconnect`, as it is more [browser-compatible](https://caniuse.com/?search=dns-prefetch).

> Make sure you do not over use `preconnect` with more than 3-4 domains as establishing and keeping an open connection can end up being too expensive, use `dns-prefetch` instead.


### ⏲️ Async/await - don't wait if you don't need to!

```javascript
function getContent() {
  return fetch('https://example.com/api/content.json')
  .then((response) => response.json())
  .then((data) => console.log(data));
}

function getTermsAndConditions() {
  return fetch('https://example.com/api/tc.json')
  .then((response) => response.json())
  .then((data) => console.log(data));
}

async function getDataForMyWebsite() {
 const content = await getContent()
 const tos = await getTermsAndConditions()

return { content, tos }
}

getDataForMyWebsite() // ❌ don't do it like that!

```

In the snippet above, you have no reason to wait until `getUser` completes in order to run `getTermsAndConditions`, as you don't use data from the previous request in the next one

To make this process much faster, run these two requests simultaneously:

```javascript
async function getDataForMyWebsite() {
 const [content, tos] = await Promise.all([
  getContent,
  getTermsAndConditions,
 ])

return { content, tos }
}

getDataForMyWebsite() // ✅ this is much faster!
```


### 👷 Web Workers

When you find your main thread constantly blocked, Web Worker is your perfect solution. In fact, [Tizen encourages you to use them!](https://docs.tizen.org/application/web/guides/w3c/perf-opt/web-workers/)

> Don't confuse Web Workers with Service Workers which in the case of Smart TVs cannot be served from filesystem, the same as module scripts. You might find the same info about Web Workers, but Tizen does allow for it.


Before adding browser API logic to the Web Worker, make sure it is [supported](https://caniuse.com/) or look for 'Available in workers' table row in [MDN browser compatibility tables](https://developer.mozilla.org/en-US/docs/Web/API/Cache#browser_compatibility). Some APIs (like DOM API or the `window` object) are not available by default.
Libraries like [ComLink](https://github.com/GoogleChromeLabs/comlink) will make your Web Worker experience much more effortless. [This article by Surma will introduce you to and showcase Web Workers in action](https://surma.dev/things/when-workers/).

### Conclusion

Designed with the sole purpose of displaying video and audio content, Smart TVs absolutely are not capable of running complex tasks or software in the same way that a computer or a smartphone can. This post has covered most of the concepts I used trying to improve the performance of Smart TV apps, though I do think these could be applied to any project that has performance issues on low-end devices. If you believe there is anything to add or complement, feel free to share!