This case study describes a step by step workflow of debugging and improving INP
in React used by Trendyol by leveraging Google tools such as PageSpeed
Insights (PSI), Chrome DevTools, and the scheduler.yield
API.
Two critical components of any ecommerce website are the Product Listing Page (PLP) and the Product Detail Page (PDP). Ecommerce traffic often comes from product listing pages, whether through email campaigns, social media, or advertisements. As a result, it’s critical to ensure that the PLP experience is carefully designed to reduce the time it takes to make a purchase. Prioritizing user experience quality is essential to achieve success. Research publications such as Milliseconds Make Millions have already revealed the significant impact of web performance on consumers' willingness to spend money and engage with brands online.
Trendyol is an ecommerce platform with around 30 million customers and 240,000 sellers, which has propelled us to become the first business in Turkey with a valuation of over $10 billion, and one of the top ecommerce platforms in the world.
To achieve its goal of providing the best possible user experience at scale while maintaining flexibility of content and working with an older version of React, Trendyol focused on Interaction to Next Paint (INP) as a key metric to improve. This case study describes Trendyol's journey of improving INP on its PLP, resulting in a 50% reduction of INP and a 1% uplift on the search result business metric.
Trendyol's INP investigation process
INP measures a website's responsiveness to user input. A good INP indicates that the browser is able to quickly and reliably respond to all user inputs and repaint the page, which is a key component of a good user experience.
Trendyol's journey to improve INP on its PLP began with a thorough analysis of the user experience prior to any improvements being made. Based on a PSI report, the real user experience of the PLP had an INP of 963 milliseconds on mobile, as shown in the next figure.
To ensure good responsiveness, site owners should aim for an INP below or at 200 milliseconds which means that, at that time, Trendyol's INP was in the "poor" range.
Luckily, PSI provides both field data for pages included in the Chrome User
Experience Report (CrUX) and detailed lab diagnostic data. Looking at the lab
data, Lighthouse's JavaScript execution time audit suggested that the
search-result-v2
script was occupying the main thread for more time than other
scripts on the page.
To identify real-world bottlenecks, we used the performance panel in Chrome DevTools to troubleshoot the PLP experience and identify the source of the issue. Emulating mobile performance with a 4X CPU slowdown in Chrome DevTools revealed a 700-900 millisecond long task on the main thread. If the main thread is occupied with other tasks for longer than 50 milliseconds, it may not be able to respond to user input in a timely manner, resulting in a poor user experience.
The longest task was caused by an Intersection Observer API callback on the search results script inside a React component. At this point, we started looking at breaking up that long task into small chunks to give the browser more opportunities to respond to higher-priority work—including user interactions.
It turns out that using the setState
operation which triggers React
rerendering inside the Intersection Observer callback comes at a high cost,
which may be problematic for low-end devices by occupying the main thread for
too long.
One method developers have used to break up tasks into smaller ones
involves setTimeout
. We used this technique to postpone execution of the
setState
call into a separate task. Although setTimeout
allows deferring
JavaScript execution, it doesn't provide any control over the priority. This led
us to join the scheduler.yield
origin trial in an effort to guarantee the
continuation of our script execution after yielding to the main thread:
/*
* Yielding method using scheduler.yield, falling back to setTimeout:
*/
async function yieldToMain() {
if('scheduler' in window && 'yield' in scheduler) {
return await scheduler.yield();
}
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
/*
* Yielding to the main thread before changing the state of the component:
*/
const observer = new IntersectionObserver((entries) => {
entries.forEach(handleIntersection);
const maxNumberOfEntries = Math.max(...this.intersectingEntries);
if (Number.isFinite(maxNumberOfEntries)) {
await this.yieldToMain();
this.setState({ count: maxNumberOfEntries });
}
}, { threshold: 0.5 });
Adding this yielding method to the PLP code resulted in an improved INP, as the main long task has been split into a series of smaller ones, which allows for higher priority work—such as user interactions and subsequent rendering work—to take place sooner than they otherwise would have.
Note that Trendyol uses the PuzzleJs framework to implement a micro-frontend architecture using React v16.9.0. With React 18, the same performance could be achieved, but for a number of reasons, Trendyol is unable to upgrade at this time.
Business results
To measure the impact of the implemented INP improvement, we ran an A/B test to see how business metrics were affected. Overall, our changes to the PLP resulted in a significant improvement, including a 50% reduction of INP as well as a 1% uplift on click-through rates from the listings page to the product detail page per user session. In the following figure, you can see how INP improved on the PLP over time:
Conclusion
Optimizing INP is a complex and iterative process, but it can be made easier with a clear workflow. A simple approach to debugging and improving your website's INP depends on whether you are collecting your own field data. If you are not, PSI and Lighthouse are a good starting point. Once you have identified pages with issues, you can use DevTools to dig deeper to attempt to reproduce issues.
Yielding to the main thread from time to time to give the browser more
opportunities to do urgent work will make your website more responsive, ensuring
that your customers are having a better user experience. Newer scheduling APIs
like scheduler.yield()
make this task easier.
Special thanks to Jeremy Wagner, Barry Pollard, and Houssein Djirdeh from Google, and Trendyol's Engineering Team for their contribution to this work.