DEV Community

Cover image for Handling performance issues in Smart TV applications
Ivan
Ivan

Posted on • Updated on

Handling performance issues in Smart TV applications

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

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 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) is rarely a good choice, as it always runs on the main thread.

Another useful CSS property is 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 and this amazing gist will be of great help.

πŸ–– Code-splitting and lazy-loading Javascript

Component 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:

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

const myComponent = component();

export default myComponent;
Enter fullscreen mode Exit fullscreen mode
// 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);
})
Enter fullscreen mode Exit fullscreen mode

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

<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-->
Enter fullscreen mode Exit fullscreen mode

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.

🧺 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:

<link 
 rel="preload" 
 href="/yourFontDirectory/your-font.woff2" 
 as="font" 
 type="font/woff2" 
 crossorigin
> 
Enter fullscreen mode Exit fullscreen mode
  • Swapping out fonts on load 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 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 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.

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!

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!

Enter fullscreen mode Exit fullscreen mode

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:

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

return { content, tos }
}

getDataForMyWebsite() // βœ… this is much faster!
Enter fullscreen mode Exit fullscreen mode

πŸ‘· Web Workers

When you find your main thread constantly blocked, Web Worker is your perfect solution. In fact, Tizen encourages you to use them!

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 or look for 'Available in workers' table row in MDN browser compatibility tables. Some APIs (like DOM API or the window object) are not available by default.
Libraries like ComLink will make your Web Worker experience much more effortless. This article by Surma will introduce you to and showcase Web Workers in action.

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!

Top comments (8)

Collapse
 
johnpaulharold profile image
John-Paul Harold

some interesting ideas here. Have you considered using position: absolute for most everything to avoid DOM thrashing?

Collapse
 
vanyaxk profile image
Ivan

I do agree that it makes sense to use position:absolute on elements that change often, so that we control height shrinking, toggling display will have a similar effect. But in my view, using it everywhere is a bit of an overkill 😁

Collapse
 
johnpaulharold profile image
John-Paul Harold

Do you also consider bundle size and transpiling when writing for Smart TV? Like, avoiding for..of loops, or avoiding async/await as in my experience they transpile to larger output code.

Collapse
 
vanyaxk profile image
Ivan

I do consider it, but you would still have to include core-js as an entry for older TVs to be compatible, so the bundle would include the whole core-js module you are importing.

Collapse
 
johnpaulharold profile image
John-Paul Harold

I think, but would have to check, you can selectively include parts of core-js

Thread Thread
 
vanyaxk profile image
Ivan

you can import bits of it, but you only tree-shake the modules that you don't use. meaning if you import core-js/stable the rest of the modules in the library won't be included in the bundle, but the whole core-js/stable will be compiled into your bundle

Collapse
 
johnpaulharold profile image
John-Paul Harold

Do you avoid frameworks, like React, Solid etc when writing for Smart TV?

Collapse
 
vanyaxk profile image
Ivan

I used vanilla and React, looking into Solid - React hasn't been a big performance bottleneck, although it's a heavy library