This article has been originally posted on Preply's engineering blog.
To improve our two most important pages from an SEO and SEM perspective, we started digging into how to improve their INP, a metric to assess web pages’ interaction speed. It was an R&D project driven by a clear goal baked by tons of data and assumptions. We spent a lot of time identifying what to optimize and made one-line changes and big refactors. We jumped onto React Server Components and Next.js App Router. We succeeded but also failed frequently. In this article, we will share the whole journey and the takeaways.
Introduction
Why care about page speed?
Page speed is crucial for web pages because:
From an SEO perspective, page speed is a ranking factor for search engines.
From an SEM perspective, page speed can influence your Quality Score on platforms like Google Ads. A higher Quality Score can lead to lower cost-per-click (CPC) and better ad ranks.
From a user perspective, a faster page speed can lead to better user engagement and higher conversion rates.
Preply.com is a B2C and B2B tutoring platform, so SEO and SEM optimizations are part of our everyday job. Page speed impacts the effectiveness of our efforts in these two areas. Page speed is measured through Web Vitals, which quantify a website’s user experience. One of the Core Web Vitals is INP, the worst Web Vital of Preply.com.
What is INP?
The official definition of Interaction to Next Paint (INP):
INP is a metric that assesses a page’s overall responsiveness to user interactions by observing the latency of all click, tap, and keyboard interactions that occur throughout the lifespan of a user’s visit to a page. The final INP value is the longest interaction observed, ignoring outliers.
Practically speaking, in a simplified way, and thinking of a “normal” MPA (Multi-Page application):
A user visits a web page and interacts with it.
INP measures, in milliseconds, all the synchronous JavaScript executed after the interaction.
The longest interaction is the user session’s INP for the web page.
The overall page’s INP is the 75th percentile of all the user sessions’ INP, split by desktop and mobile devices, over the last 28 days.
Some key things to keep in mind:
It doesn’t matter what the users do on the web page. Some close the privacy banner, and some spend 15 minutes on a page doing many things. The slowest interaction is their INP.
Users don’t have the high-end devices we typically use to develop digital products. Regarding mobile devices, some users have very old and very slow smartphones (the number of these heavily depends on the markets you operate in). So your users could deal with some performance issues you were probably unaware of. The slower the device, the worse the INP.
Thanks to the recent Google Chrome updates, working locally with INP is quite straightforward, look at the following screenshot of the devtools’ Performance tab, which includes INP.
The Google Chrome devtools’s Performance tab shows the Core Web Vitals for the current session, especially INP, and the slowest interaction that caused it.
Preply.com’s tech stack
All Preply.com’s indexed pages are managed through a Multi-page Next.js 13 application (React 18) rendered on the server through the Pages router. The codebase is quite big and has accumulated a lot of technical debt.
The Next.js app has thousands of indexed pages, but the two most important ones, from either an SEO or SEM perspective, are the Home page and the Search page. They are two very different pages:
The Home page has few lines of code and is mostly static. The only interactive elements are a slider and the privacy policy banner.
The Search page is huge, complex, and 90% interactive, and it includes modals to filter tutors, and book lessons.
Web Vitals on both pages are ok’ish, but there’s room for improvement on mobile devices’ INP. The project’s goal is to improve INP from the yellow zone to the green one.
Our internal dashboard reports the INP for the Home page (~250 ms before the optimizations, 185 ms after the optimizations) and the Search page (~250 ms before the optimizations, 175 ms after the optimizations).
We calculated that moving the two pages INP in the green zone could save us $200K/year. Please note that’s a ballpark number we identified by analyzing our current SEO situation, our competitor’s Web Vitals and estimating how much we can improve there.
Our initial hypotheses
We had two main hypotheses on how we could have improved INP:
We need to reduce React’s hydration to get the pages ready and interactive sooner.
We need to improve the performance of our own JS/React code.
Every change we introduced was the result of extensive analysis. Performance optimizations on existing big products usually require weeks of analysis and data intersection to find the root issues and only hours to fix them. This project is no exception: we started with many unknowns, and we moved through them, failure by failure, before finding out how to improve INP.
Reducing React hydration
Hydration is React’s process of making static HTML interactive on the client side. This is a common step for every SSR (Server-Side Rendered) website. Before hydration, the React app was not interactive. After hydration, it is fully interactive. But what’s in the middle?
When users interact with the page, and hydration is in progress, React records and replays their interactions. This is very convenient from a developer perspective, but it comes with a cost: interactions’ INP is worse than normal. The next graph shows how a hypothetical 80-ms INP interaction becomes a 235-ms INP interaction if it happens during the hydration phase.
The diagram shows how a usually fast interaction can become slow during hydration.
The above is an example. We can’t tell if the full hydration time is added to INP, but our manual tests showed that some delay is added to the normal INP.
The most hyped recent React feature is Server Components (RSC). Thanks to RSC, you can selectively make part of your page static, executing it only on the server, without the client-side hydration, similar to how good old PHP does. Next.js supports RSC through the App Router, and migrating the two pages we worked on was the initial big bet.
Let me anticipate one key conclusion: by looking at our metrics, we found out hydration should not be a big INP offender since it’s happening quite fast (120 ms on average), and we expect most of the users to interact after this quick phase:
At the time of writing, the INP for the Search page is 248 ms, and React hydration is 120 ms.
Please note that our app leverages React 18, which introduced various Hydration improvements. We will touch this topic later in the article.
Despite that, we migrated the Home page to the Next.js App Router anyway to gain experience. Ultimately, one of the project’s most important goals was to collect all the possible takeaways to determine the best approach for all the other existing pages in the future.
All the data we leveraged to improve INP
The “What do the users do on the web pages? What do they interact with?” questions are crucial to determining what to optimize. To find out, we needed a ton of data. Preply is a very data-oriented company; we had all the data upfront. Without all this data, bringing INP to the green zone in less than a quarter would have been impossible.
To improve INP, we used:
- The official web-vitals library is from the Google Chrome team. We collect all the Web Vitals data at every session and create a global dashboard in DataDog to show the Web Vitals data.
The generic DataDog dashboard we built on top of web-vitals library’s data.
Next.js’ custom metrics (especially the hydration and render duration).
All the mentioned data is available and queryable in SnowFlake, so everyone in Preply can analyze it and create custom dashboards.
Hotjar, to show and analyze the heat maps.
The standard browser dev tools for profiling. React Developer Tools and React Scan to catch unnecessary re-renders.
For your information, we removed DataDog’s RUM and Sentry Web Vitals because they were redundant compared to point 1 (the web-vitals library). We tried them out, but in the end, we all rely on our custom Snowflake instance when it comes to querying and aggregating data (all data passes through Snowflake, and then we forward some data to DataDog), so it was a natural choice to keep everything centralized and remove redundancy.
One impactful detail regarding the data the web-vitals library tracks: one of our most important dashboards showed a high number (75%) of <unidentified>
selectors. This defeats the purpose of the dashboard itself. The reason is a known Google Chrome issue, which makes it harder to collect the selector of the DOM elements when they are part of modals that disappear right after interactions. The web-vitals authors suggested working around this issue in this GitHub comment. Thanks to all the data we intersected, we were able to improve INP even without fixing the selector issue.
The custom dashboard with the selectors of the INP offenders shows a very high number of unidentified elements.
How we improved INP
The list of changes that improved INP on the Search page:
Change | INP before | INP after | Net improvement |
---|---|---|---|
Upgrading React from 17 to 18 | ~460 ms | ~320 ms | -140 ms |
Virtualizing a long list | ~320 ms | ~280 ms | -40 ms |
Debouncing keyboard events | ~280 ms | ~260 ms | -20 ms |
Memoizing heavy components | ~260 ms | ~230 ms | -30 ms |
Fixing state management non-EU privacy policy banner | ~230 ms | ~215 ms | -15 ms |
Avoiding the tooltip to re-render the whole page | ~215 ms | ~205 ms | -10 ms |
Updating Usercentrics for the EU privacy policy banner | ~205 ms | ~200 ms | -5 ms |
Re-optimizing what we already optimized | ~200 ms | ~185 ms | -15 ms |
The list of changes that improved INP on the Home page:
Change | INP before | INP after | Net improvement |
---|---|---|---|
Rewriting the Slider and Improving the Accordion components | ~250 ms | ~220 ms | -30 ms |
Updating Usercentrics for the EU privacy policy banner | ~220 ms | ~180 ms | -40 ms |
Migrating the Home page to the App router | ~180 ms | ~170 ms | -10 ms |
We didn’t see any meaningful improvement by:
Leveraging React’s
useDeferredValue
.Optimizing the Design System components.
Cleaning up the Search page’s code.
Quickly optimizing Preply Chat for the logged-in users.
Removing DataDog RUM.
Finally, what we didn’t do:
Offloading the third-party scripts with Partytown.
Jumping on the new React Compiler.
You can find more details in the next sections.
React 18 upgrade (INP decreased by 140 ms)
In reality, this was not part of the late 2024 page speed project this article is about, but of course, it’s worth mentioning. At the beginning of 2024, all the Product teams, coordinated by Preply’s Devex team, worked together to upgrade React to v18. This upgrade promised to bring meaningful hydration and performance improvements by itself, even without leveraging React Suspense, and so it was. In the days after deploying the version bump, we started seeing a much better INP.
The INP of the Search page before the React 18 migration (~460 ms) dropped significantly immediately after the React 18 upgrade was released (to ~323 ms).
The “Understanding Hydration in SSR with React 18’s New Architecture” article provides a longer explanation of React 18's hydration improvements.
Virtualizing a long list (INP decreased by 40 ms)
The Search page’s longest list is the Tutor’s Country of Birth. It lists ~300 countries.
The Country of Birth’s UI on Preply’s Search page.
It doesn’t sound like a huge list. Still, the 300 React components and their sub-children take significant time to be added to the DOM on a slow smartphone (please note: we are speaking about the React->DOM reconciliation that adds all the new elements to the DOM, not the render step itself). We used React Virtuoso since it was already used in our front-end projects, but virtualization libraries are all quite similar.
FYI: We discarded it because it’s not yet fully supported by the browser Preply.com supports, but CSS content-visibility has great potential to leverage CSS native virtualization. You can read more in the “Improving rendering performance with CSS content-visibility” article.
Debouncing keyboard events (INP decreased by 20 ms)
Thanks to the data we collected through the web-vitals library, we split the keyboard events, which showed a constantly bad INP. There are three input fields on the Search page, and typing something there was painful on a slow device. As you can see in the next screenshot, debouncing them quickly brought noticeable INP improvements.
Before: 632 ms INP while typing on the main learning subject’s input field, with most keyboard events resulting in a >300 ms INP. After, the same events result in a 72 ms INP.
Memoizing heavy components (INP decreased by 30 ms)
Two big components, namely the “How Preply works” and the “Users’ reviews” component, took a lot of time to re-render, increasing the INP of the interactions that resulted in whole page re-renders (for instance: when you close the Filters modal and the new filters are applied) You can see their impact in the next screenshot of the React DevTools’ profiler.
The React DevTools’ profiler, in flamegraph view, shows the impact of the HowPreplyWorks and Reviews components.
Improving INP here was straightforward: the two components were static, and memoizing them was enough to improve INP.
Fixing state management non-EU privacy policy banner (INP decreased by 15 ms)
The issue was, again, a “from global to local React state” problem. Every new user’s first interaction is with the privacy banner. On both the Home and Search pages, the banner shown to non-EU users re-renders the whole page when closed. The issue was quite straightforward: the React state to render it or not was stored at the page level, way too high in the React tree. An intermediate component containing only the privacy banner logic (to save the preference in the local storage, then hide the banner) was enough to improve INP.
Avoiding the tooltip to re-render the whole page (INP decreased by 10 ms)
We noticed something weird: the first interaction with the Search page (not the Home page) is slow. It didn’t matter what you interacted with — the first interaction with the page was always slow, but subsequent interactions were faster. The real aha moment was when we realized that even touching a non-interactive element (like the page’s background) had a bad INP.
How we proceeded:
Through the browser’s performance tools, we recorded an interaction that clearly showed the issue.
The recording showed the issue was with the
touchstart
event only.We removed all the
touchstart
handlers on the main DOM elements (html
,body
,<div id="__next">
) one by one.
The browser dev tools visualize the event handlers, allow you to remove them, and jump to the listening JS module. Initially, there were more than 30 listeners.
All the handlers were responsible for part of the slow INP, but one of them caused the whole page’s re-render.
In the Event Listener tab, the browser dev tools initially pointed us to Sentry’s listeners (which can also be used to measure Web Vitals, by the way).
After disabling Sentry, the issue remained, and the Event Listener tab pointed us to a custom tooltip we built on top of Radix UI’s Tooltip.
The reason was a basic TypeScript oversight:
The initial tooltip’s state was
const [open, setOpen\] = useState(props?.open)
which means open isboolean | undefined
.The
onClickOutside
handler (called when you click anywhere) didsetOpen(false)
. So open switched fromundefined
tofalse
, resulting in a children re-render.We fixed the issues just by changing the initial state
const [open, setOpen] = useState(props?.open ?? false)
, removingundefined
from the possible states.
As a rule of thumb, I always suggest “help TypeScript to help you” by reducing the possible types: Unions instead of generic types, Discriminated Unions instead of optional properties when possible, no falsy types mixed with booleans, etc. You can read more about this topic in my “How I ease the next developer reading my code” article.
Updating Usercentrics for the EU privacy policy banner (INP decreased by 5 ms for the Search page, 40 ms for the Home page)
For the EU users, we leverage Usercentrics. Specifically, we were using the V2 version. Similar to the previous point, the privacy banner was okay from an INP perspective, apart from when you closed it without accepting/changing privacy preferences. Closing the banner executed a huge amount of JS code that was out of our control and impacted many of Preply’s users.
The Usercentrics team suggested we migrate to their V3, which solved this INP issue. But to do that, we also needed to update the GTM (Google Tag Manager) version to load the Usercentrics banner. We teamed up with those responsible for GTM and helped them with the migration, dropping another INP offender that impacted all of Preply.com’s pages.
It’s worth noting that updating Usercentrics significantly impacted the Home page, where the privacy banner is one of only a few interactive elements. However, the effect on the highly interactive Search page was minimal.
Re-optimizing what we already optimized (INP decreased by 15 ms)
The improvements mentioned above eliminated the worst INP. Then, we had a few weeks (see the graph) with no noticeable improvements. We were hitting the end of the 80/20 Pareto rule, and all the next fixes would have brought lower benefits while requiring more implementation time (which we didn’t have, the project was meant to be completed in 2024).
The INP graph shows that after many quick wins, we had no improvements for a few weeks.
We then decided to return to the already optimized issues and optimize them even more. Ultimately, they proved to be low-hanging fruit because they impacted the core user flows, why not optimize those flows even more?! So we started using a 20x CPU slowdown to measure performance improvements, which is surely closer to the budget devices we are optimizing for compared to the previous 6x slowdown (a 6x slowed-down high-level MacBook Pro is still faster than most devices on the market, especially when speaking of mobile devices).
It worked, and we could push down INP even more and match the project’s OKRs 🎉.
Migrating the Home page to the App router (INP decreased by 10 ms on the Home page)
Next.js App Router and RSC (React Server Components) improved INP only slightly. We expected more, but it was a mistake of ours: in our initial POC with App Router, we wrongly used TBT (Total Blocking Time) as a proxy metric for INP, that’s why our expectations were erroneous.
But there’s more to tell! Just saying “App Router improved slightly” is a bit unfair because its potential is higher. Why? Well, I told you that we migrated, not rewritten, the Home page!
So, we built a POC with a greenfield Next.js app. We chose only RSC-compatible libraries (for internationalization, for example), and rewritten part of the page with data-caching in mind. The results (performance is measured with a 3G network and 20x CPU throttling, locally, in prod mode) are impressive:
Pages Router | App Router | Improvement % | |
---|---|---|---|
JS bundle size | 1.2 MB | 173 KB | 85% less JS shipped |
HTML response time (shows how fast the server generates and sends the HTML) | 51.8 s | 15.7 s | 70% faster server execution |
Web Vitals: FCP | 5.8 s | 1.9 s | 65% better |
Web Vitals: LCP | 5.8 s | 2.8 s | 50% better |
Web Vitals: INP | Most interactions: 80-100 ms | Most interactions: 30-40 ms | 60% better |
These results give justice to the App Router! Then, we all agreed that the optimal and long-term solution (not feasible in the project’s lifespan) would be to start from a fresh Next.js project and rewrite all the features hacked into Next.js in years and years of fast growth.
useTransition, await-interaction-response, etc.
During the project, we tried to go deep, find the root cause of the issues, and fix them. We didn’t want to hide the dust under the carpet and worsen the problem.
Despite that, we encountered situations where the lack of proper design wasn’t fixable in the project’s lifespan, and “lying” to the browser was the only way to speed up some interactions. What I mean by “lying”:
In the great Demystifying INP: New tools and actionable insights article, Vercel (Next.js’ creators) shared the await-interaction-response package. This is the source code:
/**
* Returns a promise that resolves in the next frame.
*/
export default function interactionResponse(): Promise<unknown> {
return new Promise((resolve) => {
setTimeout(resolve, 100); // Fallback for the case where the animation frame never fires.
requestAnimationFrame(() => {
setTimeout(resolve, 0);
});
});
}
Essentially, it’s a small hack to avoid blocking the main browser’s thread and postponing the execution of your heavy code. This improves INP, of course, but in our case, it worsens the problem because it’s a sort of “Don’t worry, the lack of design and development anti-patterns are fine, just use this workaround” message, which we don’t want to spread.
React’s useTransition is more elegant because it’s embedded straight in the framework, but in our case, using it hides the root problems instead of helping/teaching how not to introduce them upfront and removing the existing performance bottlenecks.
But for those looking for short-term and quick solutions… well, keep them in mind 😅.
Just one note: if you are considering implementing a custom yielder, please don’t rely on requestIdleCallback
! When working on the Design System Visual Coverage, we saw that the browser frequently invokes the passed callback after several seconds due to other intensive processes. In The Implementation Details of Preply’s Design System Visual Coverage article, you can read all the performance optimizations we made there.
Leveraging React’s useDeferredValue (no INP improvements)
We were a bit surprised this didn’t work. React’s useDeferredValue keeps input fields snappy when the result of the users’ typing (the list of languages to learn, in our case) requires a significant amount of time to be updated on the page. After refactoring one of our input fields, we noticed no improvements compared to when the input field’s value was debounced. But why?
We realized that useDeferredValue works as a charm when the items to render are heavy at the React rendering phase (essentially, when React executes the component’s functions that return the JSX) but soft on the DOM reconciliation phase (where React adds/updates/removes the elements from the DOM). Our case was the opposite, the components are fast enough at rendering but heavy regarding how many DOM elements they produce (you can read a quick thread on X about this), vanishing useDeferredValue’s benefits.
Optimizing the Design System components (no INP improvements)
Path (Preply’s Design System) is used a lot: the Search page counts 875 usages of the Path’s components, and 76% of what the users see comes from Path; you can dig more into how we collect the stats in the Visual coverage: Why and How Preply Measures the Impact of the Design System article. A particularly heavy part of the Search page’s render tree highlighted that the two most used Path’s components (LayoutFlex and Text) had room for improvement when transforming the React props into CSS class names, impacting the overall page’s INP.
Refactoring the Path’s components doesn’t happen overnight, and we spent days heavily testing and refactoring the LayoutFlex component to avoid any regressions across the whole website. All this heavy work created the foundations for how the Design System team can safely test and refactor the components, but from an INP perspective, it brought no improvements. Our analysis was wrong.
Cleaning up the Search page’s code (no INP improvements)
The hundreds of experiments the Product teams launched in recent years were still part of the codebase. Some were just dead code, some were imported dynamically only if the users were in one of the a/b cases, but never removed when the experiments were scaled or killed. Some were always imported, negatively impacting the JS bundle.
We removed 15% of the Search page codebase (7K lines of code) for two weeks, but this didn’t show INP improvements. This has been a recurring theme during the project (later on, we speak about the Next.js Ap Router), but shipping less JS never improved INP for our Search page (please note I’m speaking only of INP, shipping less JS is always good).
Quickly optimizing Preply Chat for the logged-in users (no INP improvements)
Logged-in users can access Preply Chat from the header, a modal that opens inside the same page. From Hotjar, we got the idea that many users were opening the Preply Chat.
A screenshot of the Search page shows the Chat button at the top for logged-in users.
We didn’t expect it, given how indexed pages and heavy SEM campaigns bring tons of new and not logged-in users to the Search page, but from some quick queries, we realized 80% of the Search page views come from logged-in users 🤯. Given that Preply Chat is complex, we used the mentioned await-interaction-response, postponing some of the Preply Chat actions. We didn’t see the INP improvements we expected, meaning that our assumptions from intersecting all the data were again wrong.
Removing DataDog RUM (no INP improvements)
After extensive local tests, and even if it looked weird, DataDog RUM seemed to be causing worse INP. Luckily, the tool was redundant for us (since we mostly rely on the web-vitals library), and we could remove it. Once more, after doing so, we didn’t notice any improvement.
Offloading the third-party scripts with Partytown (we gave up)
We load many third-party scripts, some in a developer-controlled way and ~40 through GTM (Google Tag Manager) without developer control. Despite knowing that the scripts loaded through GTM impact the page negatively, we gave up on trying to move them to Web Workers with Partytown. Here’s why:
Once loaded by GTM, some scripts append more scripts to the DOM, which are executed in the main thread, partially defeating the purpose of adopting Partytown.
Years and years of scripts added through GTM, and maybe frameworks/integrations built on top of them, almost guarantee that we break something without a clear correlation between what we break and what results are wrong.
Given how we all depend on our data and the short amount of time available for the project, we decided to give up, given the unclear return on investment of this work.
Jumping on the new React Compiler (we didn’t even try)
The React Compiler is under construction at the time of this writing. However, the limitations we discussed with the Devex team were the Next.js v14->v15 upgrade and the React v18->v19 upgrade, which required more time than ours and didn’t fit the project’s time scope. Wakelet shared an early feedback about the Web Vitals improvements they got from adopting the new compiler.
Did all this work also improve the UX?
The answer is: YES! Working with 6x and 20x CPU slowdowns, the UX improvements are evident, though there’s still room for further enhancements. INP proves to be a great metric from this perspective; it acts as a true litmus test for how painful it is to use your product when users don’t have access to top-notch devices. While all Web Vitals are valuable, INP uniquely measures the user experience throughout the entire session, regardless of whether the user’s journey is brief and superficial or deep and long.
Miscellaneous
Many of the project’s details have been omitted from this article because they are not crucial or relevant to readers. Let me just mention some quick facts/suggestions:
We saw the first INP improvement after more than one month of work, out of a quarter.
The weekends were crucial to measure the impact of our work because we saw dashboards needed 24–48 hours to reflect the INP changes.
We couldn’t stop other Product teams from running experiments on the pages due to an overlapping important Product goal. It wasn’t optimal, but the generated friction wasn’t so high.
The Devex team worked hard to upgrade Next.js from 13 to 14 and leverage the faster Turbopack for local development.
The project started with the goal of optimizing both INP and TTFB. Then the two metrics were split into two different projects.
Jumping on the App Router required us to refactor all the LESS modules to SCSS (also in the Design System).
Always validate your assumptions on more than one PC: it happened more than once that one of us identified possible improvements, but the same improvements were invisible on other laptops (because of background zombie processes, because a user becomes part of a data sampling that slows down the website, etc.).
Medium and long-term plans
All the mentioned improvements are short-term, and the benefits can vanish quickly if we don’t make performance a first-class citizen when developing the Product. We haven’t formalized all the activities for this goal, but we have some in mind:
(you saw it in action through this article) Monitor Web Vitals to catch regressions.
(already done) Add Web Vitals to our experiments framework’s dashboard and the other Product metrics we all care about.
Add performance data to fill the PR template.
(already in progress a the time of writing) Run workshops to share the takeaways and spread React best practices with all our frontenders.
Run some workshops to share how to deal with the existing Product’s refactors from a performance perspective.
Discuss the role of App Router in our product and our infrastructure around it.
Takeaways
✅ Tracking tons of RUM (Real User Monitoring) data and having it at hand is key.
✅ Adopting a 100% data-driven approach from the beginning is crucial. As you can see above, the optimizations that solved the INP problems are not rocket science, but it is important to identify them before jumping on the code.
✅ Don’t blindly follow what others did and shared (including this article); don’t take for granted that the other, more hyped framework solves your problem. Ultimately, we achieved our INP goals by speeding up our existing code.
Some more resources
The great people who worked on the page speed project
This project took a village, and I want to thank everyone involved 🤗
- Maksym Ridush, for the initial deep analysis that resulted in the page speed project 👏.
From the team responsible for the Search page:
Artem Yermak, thank you for the endless support and for improving INP 💪.
Ivan Biletskyi, for the outstanding support, reviews, and knowledge-sharing ❤️.
From the team responsible for the Home page:
Carles Ballester, for the Home page migration and working on the endless experimentation issues 🤘(and Robert Serrat Morros for supporting Carles).
Michael Castro, the Engineering Manager who’s also a front-end expert you don’t expect 😊
Alejandro Lampropulos, for the GTM and UC support 🙏.
Preply’s Design System’s engineers, who have been borrowed to the page speed project:
Oleksandr Kozlov, the real Next.js guru 🤯.
Seif Kamal, who constantly challenges and analyzes every single data 🚀.
Myself 😁.
Bogdan Brindusan, for jumping on and managing such a complex and out-of-the-comfort-zone project.
Vadym Vlasenko, for always digging into the details and challenging us.
All the people from different teams who helped us
Mate Papp for everything DevEx, including the Next.js v14 migration.
Kyryl Sablin, from the SRE team, thank you for the constant help and support.
And, of course, the reviewers who helped make this article better 🤗
Sébastien Lorber, for the suggestions, comments, and your infinite news source: This Week in React ⚛️.
Nicolas Beaussart, for the detailed review and the time you always dedicate to my articles 😳.
Andy Jessop and Omri Lavi, for your interest and comments 🤗.
Dmitry Belyaev, for reading this even if you were on vacation 🌴.
Maxi Ferreira, the author of one of my favourite newsletter: Frontend at Scale.
Nadia Makarevich, the Advanced React course’s creator and Developer Way’s author.
From improving our engineering culture to becoming an AI-native company… Would you like to join us and work in a purpose-driven organization where work, growth, and learning happen at the same time? Preply continues growing and we are actively looking for talented candidates to join our Engineering team! If you are excited about taking on a new challenge, check out our open positions here.
Top comments (1)
That's some really amazing stuff! Thanks for sharing with such details 🤗 can't wait to hear about the next incredible things you'll do!