How Trendyol reduced INP by 50%, resulting in a 1% uplift on click-through rate

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.

Trendyol's INP according to the CrUX readout in PageSpeed Insights. Trendyol's INP as of September 5th, 2023 was 963 milliseconds, which is in the 'poor' range.
Trendyol's INP as of September 5th, 2023 from PSI.

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.

A readout of sources of long tasks in Lighthouse for the Trendyol website. One major source of long tasks is a script that handles search results on Trendyol's PLP.
Trendyol's JavaScript Execution Time audit from Lighthouse as of September 5th, 2023 from PSI.

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.

A screenshot of a performance profiling session in Chrome DevTools for Trendyol's PLP. The long task depicted runs for 737.6 milliseconds, and is part of an Intersection Observer callback.
A performance profiler of long tasks on Trendyol's PLP in the performance panel in Chrome DevTools.

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.

A screenshot of a performance profiling session in Chrome DevTools for Trendyol's PLP. The long task that was previously running for 737.6 milliseconds is now split up into several smaller tasks.
Task split into smaller ones.

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:

A screenshot of Trendyol's 75th percentile INP over the course of six months. By the end of the six months, Trendyol's INP decreased to nearly 650 milliseconds from nearly 1,400 milliseconds.
75th percentile INP improvements 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.