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:
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;
// 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
<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.
π§Ί 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, like so:
<link
rel="preload"
href="/yourFontDirectory/your-font.woff2"
as="font"
type="font/woff2"
crossorigin
>
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"
orIntersectionObserver
(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 usecanplay
orcanplaythrough
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, usedns-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!
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!
π· 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)
some interesting ideas here. Have you considered using
position: absolute
for most everything to avoid DOM thrashing?I do agree that it makes sense to use
position:absolute
on elements that change often, so that we control height shrinking, togglingdisplay
will have a similar effect. But in my view, using it everywhere is a bit of an overkill πDo you also consider bundle size and transpiling when writing for Smart TV? Like, avoiding
for..of
loops, or avoidingasync/await
as in my experience they transpile to larger output code.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 wholecore-js
module you are importing.I think, but would have to check, you can selectively include parts of
core-js
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 wholecore-js/stable
will be compiled into your bundleDo you avoid frameworks, like React, Solid etc when writing for Smart TV?
I used vanilla and React, looking into Solid - React hasn't been a big performance bottleneck, although it's a heavy library