Tokopedia is an Indonesian technology company with a mission to democratize e-commerce through technology. It is one of the largest e-commerce platforms in Indonesia.
Tokopedia has various verticals where users can buy physical, digital & fintech products. We have a desktop as well as a mobile site, where most of the traffic comes on the mobile web.
A lot of users visiting the mobile site have low-end phones with 3G network quality, for the best user experience, our Tokopedia Engineering invests heavily on web performance.
The problem at hand
The sale is a core part of any e-commerce. Since 2018, Tokopedia has been doing a major sale event every year in the month of Ramadan. We call it Apollo. You can compare it to the Great Indian Festival or Big Billion day done by Amazon & Flipkart in India during Diwali or Alibaba’s Singles Day or In the US the Black Friday Sale.
Web pages used in Tokopedia’s sale are called as Discovery pages. It’s like an entry point to sales/flash sales where customers learn about products on sale & offers/discounts etc. These pages are super dynamic in nature i.e. each sale page can have different layout/styles. So, these pages should be fast and should quickly respond to user clicks & gestures.
Before I start sharing about the performance improvement that we did, here is the score that we had, performance score ranges from 67~71(as per page speed)
As per the above performance score, if you have a 3G connection on your phone, your page will take a minimum of 5–6 seconds to load and interact.
To improve the performance, we set our targets to achieve a score of 80+.
Most of the time when we develop new features, we don't notice important areas like DOM size, painting time, the main thread, etc due to bad plannings or strict timelines.
Before diving further, I would like to show, how a discovery page looks like.
The above image is for one of the discovery pages used in Tokopedia mobile, the content of this page is dynamic and is controlled via an internal dashboard that is used by the business & product team.
It has a banner, category navigation, product cards, slider, etc we call them components.
The backend API provides us the list of components that needs to be rendered on the pages, the components are more than 20 on any discovery page.
Also, these components can have a child component too, so to paint all n number of components and their child can increase main thread work, DOM size, time to interaction.
Reduce DOM size
As stated above we have too many components on the page and we wanted to reduce the DOM size and paint time.
We took an approach, not to paint every component which is not in the viewport. We added an intersection logic to determine the components in the viewport.
Let’s check the implementation
The above image is from the sale page, this page has around 25 components, and as you can see we have just 5 components in the viewport at first load.
How did we reduce DOM size?
We took an approach to paint only 8 components at the first load for any discovery page and as soon as user will scroll, we will set the value of the intersected flag to true
Here is the implementation:
As you can see in the code snippet, we exit for loop if the index (k) is greater than the threshold value (+2) and on window scroll, we update the intersected flag to true.
Live example of the HTML paint before and after scroll.
The DOM elements on the left are far less than the right snapshot, we have almost 8 HTML elements at first view, and when users scroll all the required DOM elements are painted.
The same intersection concept was also implemented for components using a horizontal scroll.
This helped us to increase the performance score index by 8 points from 62 ~ 68
What can be done more?
With Google’s Page Speed Insight auditing, we got some suggestions like preconnect, defer images, etc. Preconnect was missing for some of the URLs.
What is preconnect?
Pre-connect is enabled on a web page by adding
rel=preconnect an attribute to a
<link> tag, It informs the browser that your page intends to establish a connection to another domain. Know more about preconnect, read here.
You can add a pre-connect on your HTML page by adding a simple line of code
Learn from your mistakes
We added pre-connect tags for the missing URLs on our page, but after adding the same we didn't see any improvements on the page.
Because we didn’t put the pre-connect tags at the right place.
If you can see in the image, preload links are placed before pre-connect which eventually means pre-connect never worked for us.
Know about preload here.
So we corrected the order for pre-connect tags put them on top,
the pre-connect tags helped us to reduce almost 180~ms for one connection
Results from the webpagetest.org
Below are stats from webpagetest.org, you can see there is no DNS lookup, initial connection, etc when using pre-connect.
Reduce Image size
Our discovery pages are full of images, banners, sliders, etc, we were using jpeg, png and the cost of each image was very expensive, creating performance issues.
We decided to migrate the stack to WebP.
Make use of WebP
Every browser doesn't support WebP, like safari doesn't support,
There are two ways to make use of WebP.
Skip server rendering for images
One of the approaches is to detect the browser and fetch webP image or jpeg/png image based on the browser.
Example: Say your SSR page has a component with an
<img> tag and the path for the image is
http://test.com/image.webp when you build this
the component on the server it will fetch the WebP image by default.
The above code will work fine on chrome but on safari, it will break.
We are not sure about the user’s browser while rendering the component on the server, the possible solution is to wait and detect the browser on the client which means not to render the image component on the server.
If you want to load the WebP images, you need to render your image component on the client-side or lazy the load the component.
WebP can increase your time to interact if not properly used.
As shared above, if you are rendering the component on the client, you will have to wait for the browser to detect WebP support, and then it will start to paint it, which can increase your time to interact in most of the cases.
Let me try to explain with an example — when you have a Server-side rendered page you build some of the components on the server while some on the client.
The image on left is showing the content build from the server, and the image on the right showing the final layout after client rendering.
If you don’t know to check what is render from the server just Open developer tools in Chrome → Network tab → Doc and click preview
As you can see in the image, we are not rendering our slider component from the server when the page reaches to browser the slider is rendered as shown in the right section of the image.
After the server has rendered the HTML, the page shifts some of the components to bottom on the client since it has to paint slider on top (as shown in the right section) which will cause repaint of your DOM that will increase your Time To Interactive (TTI).
Check for WebP support on SSR
There’s another approach, Do you know we can check the request headers of the document to detect whether it supports WebP or not?
This approach is called Client Hints.
We have a request header called
accept and it tells us the browser accepts
image/webp.This image is taken from the chrome browser and if you open safari the
image/webp is not available under
We can read these headers on SSR by adding a simple line of code
global.webpSupport is just a flag to update our component that they can fetch webP or jpeg.
With the above approach, you don't need to wait for the image component to be rendered on the client you can build the image component on SSR which can give a boost to you TTI
Fetch smaller images from the server.
We use a lot of images on the discovery, we have a slider, banner, tabs, etc all these components use images, but images we get from the server are large in dimensions than required.
The images in discovery are served from the server we don't have static images, API provides us the path of the image.
As you can see in the image, each row has 2 two banners and the width of each banner image is around 400px, which means 800px on each row.
The maximum width for any device is around 414px which is nexus 5, and we were rendering two images each 400px.
We wanted to improve the load time, and the plan was to fetch smaller images, so we wrote a utility that will replace the image URL coming from API with the new image URL containing the preferred dimension required.
Example: If we have two banners in a row we will fetch images of size 250px, if we have one banner will fetch the images of size 430px.
This helped us to reduce 30% of image size on the page, sharing one of the samples.
you can easily spot the difference between the size for the same image when fetching images of smaller dimensions.
Note: The backend should support images of different sizes
Convert your Class Components into functional components
Apart from the above optimizations, there was still some room for improvement like converting your class components into functional components.
We had some of the components in the discovery that were still using class components, so we planned to migrate them into functional components and we saw noticeable differences in chunk size.
Here are the stats for a basic class component vs functional component
Our discovery~chunk was around 21KB(gzipped) as we were shipping some extra chunks on the first load.
Most of the components were not used at first load like a share button, toaster, back to top button, internet~connection, etc.
We decided to load all these components on demand and when the user will perform some action we fetch those required components.
With the lazy loading, our discovery chunk was now reduced to 14 KB.
After working on the above optimizations and much more, the result was overwhelming
Finally achieved the Performance Score we targeted for, on mobile with a fast 3G connection, we are also evolving and aiming the similar changes in other important pages like search, official-store, etc.
Last but not the least, I would like to introduce Kanishk, one of the engineers at Tokopedia’s Web Platform Engineering who was riding with me on this performance journey and thanks to Syed Haani (Engineering Manager Web Platform) for reviewing and supporting us throughout the journey.
I hope you enjoyed reading this article.
P.S. 👋 Hi, I’m Aatif! If you liked this, follow me on twitter and share the story with your developer friends.
Disclaimer: All the Performance Score shared are based on Lighthouse V5, on 28th May 2020 V6 is live on pagespeed, so scores may vary from the mentioned date.