daily.dev switched from Preact to React for its frontend framework, aiming to resolve development issues and enhance performance. The move, executed during a team hackathon, involved significant planning, testing, and codebase adjustments. This shift allowed for better compatibility with Next.js, improved development experience, and prepared the platform for future technological advancements.
We used Preact with the compatibility layer and Next.js Preact plugin to make it work with Next.js and common React features. We aliased it through our package manager to replace React with Preact libraries. We also use Vercel as a hosting platform because it natively supports many Next.js features. Since we use a lot of those features we want to make sure we keep up to date with the latest versions of the framework and also provide better DX (development experience).
- Hot reloading
- Error handling
- Overall slow environment and rendering
The last one was particularly difficult since having Preact in development mode was somehow a lot slower than we were used to. It resulted in constant freezes of browser tabs and hot reload was also really slow after any change in the codebase. This happened in all the parts of our app no matter if it was a simple component or a complex page. This was our main issue since it slowed us down during any kind of development work.
The first thing in our effort to solve the issue was disabling the compatibility plugin for Preact and Next.js, which helped with performance issues but resulted in hot reloading no longer working in our app. After the above "fix" we tried to debug further and returned the mentioned compatibility plugin but we were not able to make it work without again introducing performance issues and a slow development environment, it was just too many moving parts with custom configurations and overrides.
We also analyzed our rendering performance like amount of state changes during app initialization and re-rendering occurrences, but while we did find some opportunities to improve performance it did not solve our main issue. In the end we tried moving to basic configuration of Next.js without Preact. We decided to quickly remove all the overrides, plugins and custom configurations, this was a "make it work" solution, but it worked!
Since we knew migration needed to happen we created this POC (proof of concept) running on React to validate our assumptions. Some team members even ran that patched diff of our apps while developing new features. Feedback was that this version solved all the problems we had with our Preact setup so now we were sure it was going to work! 💪
While we knew that replacing Preact with React would fix our issues, moving from one frontend framework to another is still no small task. The main goal was to improve our development environment to make sure we can continue to deliver efficiently.
That kind of architectural change requires all-around work in many aspects of our app(s). Since daily.dev monorepo supports multiple apps (browser extension, web app and mobile PWA app) we had to make sure all of those still worked and that we would also be able to ship existing features without delays or downtime.
We also had to make sure that we didn't introduce any new bugs or regressions and that we keep our codebase clean and easy to maintain. We also wanted to make sure that we don't introduce any new performance issues and keep our bundle size as small as possible. With that in mind, we wrote down the general pros and cons of moving to React.
- React is 100% compatible with Next.js
- By using React it enables us to also use advanced Next.js features in the future and keep up with major versions like React 18 and new hooks and APIs it brings
- Less setup code to maintain, we just use default Next.js config without bundle aliases, configurations and overrides
- Less 3party libraries
- More consistent APIs, Preact is currently in the middle of supporting React and also introducing new features (like signals) from other frameworks. While this is cool it kinda makes it a kraken with multiple paths of development which for us is not ideal
- React is backed up by large companies like Vercel and Meta (Facebook)
- Bundle size increase (Preact size is much smaller than React)
- Many project changes, a lot of places reference Preact and its libraries
- Tech debt, while Preact is not much different than React it does allow for patterns like invalid hook calls and context usages that are currently passing as disabled lint rules which in React will just break the app
- Time allocation and scheduling of other development tasks during the migration
One of the main things why we chose Preact in the past is because it packs the same functionality of React in a much smaller footprint.
We tested bundle size for our production build across the page sizes and different chunks. Next.js build output also provides a good output overview so it was easy to compare before/after. Let's look at some of the main pages:
We are seeing 34kb increase across all pages for their “First Load JS”, that was the exact difference we measured between React and Preact. This is because our frameworks bundle raised for 34kb after adding React, that bundle is shared across all pages so that is why all the pages got the same increase.
It’s important to note that the “First Load JS” only applies when user lands on the specific page for the first time, after that each new page size is equal to “Size” column in the table above. Exact page sizes stayed the same (inside 1kb difference) as can be seen in the “Size” columns above. This confirms that React does not add any other overhead on our bundle size. The good thing is that frameworks bundle is predictable in size and is easily cacheable by the browser so we can partially mitigate that increase.
As a result of this analysis, we identified a few places where we can optimize our bundle size. While all of those were not implemented in the context of migration we do have them logged so we can tackle them down the line as part of our general project maintenance budget.
Taking everything of the above into consideration we as a team decided to move forward with the migration. We decided to do it as a hackathon project during our team gathering in Poland which was just coming up at that time. Since we are a remote team this kind of gatherings are opportunity to see everyone but also do some work face to face and benefit from more sync communication than what we are used to. It allowed us to bring the whole team in the room and work on the same problem together.
We figured it was perfect timing since during the gathering we would not have any other major feature deployments and all team members would be able to focus on migration. With all hands on board we had 1 week or ~4 working days since most members flew back home on Friday. This was still a small amount of time so it was crucial to do a planning phase before the gathering. That meant that when it was time to execute everyone was on the same page. We identified 3 main areas that we need to cover during the migration.
Core project changes
Those included removing all Preact overrides and dependencies. This needed to be done before any other work so each member could start from the same codebase. We also had to do the same for our browser extension since it also uses Preact but has a separate build process from our Next.js app.
We have a lot of tests in our project and we needed to make sure that we don't introduce any new bugs or regressions there, so all of them had to go green. We also had to exchange the core version of Jest that works with React.
Compatibility (pain) points
By using Preact compatibility layer many of our components will be able to transfer to React with no issues since we are using React APIs already.
That said Preact is a little loose on what can and can not be done in React. That is good because it gives you more tools but also breaks some common concepts which leads to bugs. We already had a few of them logged so it was an opportunity to make our production app better as well.
We identified them in two categories, warnings and errors. Warnings were mostly due to invalid props being passed to underlying DOM elements which did not lead to bugs but it did pollute our browser console and also made it harder to debug other issues. Errors were mostly due to invalid usage of React APIs which did lead to bugs and also broke our app. Stuff like setting state during rendering, accessing context outside of React components/hooks or memory leaks with event handlers.
Planning phase resulted in:
- DR (decision record) with all the analysis and context for the migration
- Opened epic branch and PR on our Github repo containing all the core project changes
- Set of tasks with clear instructions on what needs to be done
The week of the gathering has come, all of us met up and started hacking! ⌨️
After the first day we managed to do all the core project changes and sorted most of the warnings found around the codebase. Immediately it was evident that a good planning phase paid off since we were very efficient.
Second and third day also went on pretty smoothly. Most of the issues we had were actually with flaky tests that we discovered after plugging in the React version of Jest. We also managed to fix a few memory leaks and re-render issues just by adjusting our usage of React APIs.
On the final day of the gathering we spent on multiple reviews and E2E (end to end) testing of our app to make sure all the core user flows were working. We also did a final bundle size analysis to make sure we did not introduce anything there.
Each team member also did a final review of all the changes in the epic branch. In the end you might be disappointed to hear we did not merge everything to the production on Friday 😂, but we made sure to celebrate! Overall the atmosphere and accomplishment were great 🍻. Finally, we did merge it to production week after gathering without any issues.
What is next?
We started this migration because for months we had problems with bad DX that blocked our team from developing and delivering new features 😥. We tried many solutions but what worked in the end was structure, planning and great teamwork. ✅
While React migration did require major effort it was a success and enabled a healthier future for all our apps! 🚀
Preact compatibility blocked us from upgrading to the latest versions of our core libraries like Next.js. React will allow us to move forward and adopt features that will make the product better 🙌. While major upgrades to other libraries did not happen during our switch to React we are excited for what is to come next.
If you wanna go through the replay of everything we did you might be happy to hear that our monorepo is open source so you can take a look at the main and all other related pull requests here. Enjoy! 🎉