Giftpack.io is a global on-demand gifting service that provides customized gifting experiences to its customers in 3 hours worldwide. The website was launched along with the foundation of Giftpack back in 2017, and has grown in size as more features were brought in. As time goes by, the application starts to slow down, shows poor performance, and now became extremely hard to maintain or even introduce new features. Many of the dependencies and packages used in the project were outdated, and upgrading each and every one of them one by one would cost way too much time and effort. That is why we have come to the decision of destroying everything, and rebuild the entire front-end of Giftpack.io from scratch.
One of the main reasons why we were not able to rebuild the application in the past was due to lack of numbers of engineers working and maintaining it. Since I have joined Giftpack in February 2020, I had been fully responsible for every new feature as well as the maintenance of the application itself. As someone who loves using the latest front-end technologies, it was a painful process while getting more familiar with the codebase. The application was built on top of pure SSR React.js using Node.js and Express.js. Here are some of the core technologies that the project was using back then:
React Redux 5
React Router 3
React Router Redux
ReduxAsyncConnect for React Router
I still clearly remember one of the first tasks I was assigned with right after I joined: upgrading React to the latest version (16.12.0). And trust me, it took me around 2+weeks for that to happen without breaking the app. Imagine if I had to also upgrade for all other core dependencies such as React Router (which had breaking changes after version 4), I would have to spend (or waste) most of the time finding compatibility issues without being able to add new features.
Another main reason to rebuild the project was the quality of code. To be blunt, it was just spaghetti code: code files aren’t formatted, useless commented code everywhere, inconsistent coding style, repeated code blocks without being written as functions to be reused, confusing lines of code without any comment, chaotic service API calls, lack of modularity, all the bad stuff you can imagine. One funny thing about the codebase is that the way Redux was being used makes the UI states so unpredictable (which ironically is what Redux intends to solve).
Considering the low number of front-end engineers at Giftpack, it surely is not a wise choice to find another SSR boilerplate or set up intricate configurations for our project anymore. Rather, it would be nicer to choose a framework that handles all of that. The current most popular SSR React framework would undoubtedly be Next.js by Vercel, which totally fits our needs.
As we migrate our application to Next.js, we are also fundamentally refactoring the project at all levels. Here are some of the crucial things that are considered before the beginning of the migration:
Since the entire application was written before React 15, all stateful React components are written as class-based components. Through this opportunity, all components are being re-written into functional components with the help of React hooks, which was officially introduced after React 16.8. This greatly increases the readability of the code, and significantly decreases the number of LOCs. Writing reusable custom hooks also helps increasing the modularity of our code.
Next.js has a file-system based router built on the concept of pages, so React Router was no longer needed in our project. However, this would heavily influence the architecture of the application, since it made use of React Router Redux as well as ReduxAsyncConnect in order to pass server props to the client. One thing I still could not understand about our codebase is that it does not simply pass server props directly to the individual components, but rather dispatches actions to change the Redux state, and then passes the state to the components. Because of this, every single reducer is extremely complicated, and each file that describes a reducer would exceed at least 200~300 LOCs. This applies to the action creators as well. The solution for this is very simple in our case: abandon any use of Redux for passing server props and make use of getInitialProps to keep the server props’ logic only within the component itself.
The use of Redux in the application was mostly focused on dealing with server props and page routing. Thanks to the new routing architecture, we can simply abandon around 95% of our Redux state. There are only a few cases which we still have the need to use a global state: user information (authentication status) and order information (payment checkout). This really keeps the reducers nice and clean, and makes the states much more predictable (as it should be).
The way how the application calls API services was totally unorganized and messy. To improve that, all API-related operations are organized and categorized under a folder named /services. Here is a sample API call to login a user:
By organizing them in such way, any logic involving server requests will be separated from the UI components, allowing the components to focus more on presenting the view instead of the details on how to call the APIs.
As Giftpack supports multiple languages, i18n is a required feature. However, it had been poorly handled in the past: when a user enters the website, every single locale text file is being fetched in the server, stored into Redux store, and passed into the client altogether, regardless of what the page needs. Because of the heavy load, each time a user switches the language it would take up to 5 seconds to load, which is obviously a terrible user experience. Luckily before the migration, I have already added react-i18next to handle localization both on the server side and the client side. In Next.js, there is the next-i18next module which makes use of react-i18next already, so it was simply a smooth transition.
Overall, these are all things that should be carefully considered at the same time in a parallel manner during the process of refactoring. To give you an idea of how much difference there is, here is a sample comparison of our landing page dealing with server props:
(I had formatted the code and removed some of the useless commented lines in advance, so it was originally even messier than this.)
And we are only talking about handling server props here. After transforming class-based components into functional components with hooks, almost every single file loses around 40% of its original size, in terms of LOCs.
Although not being able to maximize optimizations in every aspect, the refactored version surely has much greater performance than the old one.
By default, Next.js handles route-based code-splitting on its own. At the same time, it also supports dynamic imports which would allow us to split our components and modules into separate chunks according to our need. In this way, heavy components and modules which are not necessarily critical during the initial render of the page can be easily lazy-loaded.
In the old version of Giftpack.io, there was no implementation of code-splitting at all. We are now implementing it whenever possible in order to have a better user experience on our website. Here’s one simple example to demonstrate:
Inside our current product checkout page, there is a section where a customer can choose to add special requirements according to his/her demand. We use a component named
The problem is, for a user who does not need to have any special requirement, we are basically wasting internet resource to request the component from the server, even though this component is not needed at all. Even if it is needed, we are still wasting the time for the initial render because the user only has to see the section after he/she clicks the button.
With Next.js’ dynamic imports, we can very easily make the component lazy-loaded without interfering the initial render:
import dynamic from 'next/dynamic'; const SpecialRequirements = dynamic(() => import('components/SpecialRequirements'));
According to the **docs**, you may also optionally provide a fallback UI in case the component is still being loaded.
If you are already used to data-fetching with server APIs, you would most probably be comfortable with code-splitting. It is nothing more than an operation which “fetches” components or modules in an asynchronous manner.
WebP is a modern image format that provides superior lossless and lossy compression for images on the web
You may have a look at **Google’s Introduction** on Webp to have a better understanding about its advantages as well as how to use it in your projects.
By converting our images to Webp, we are able to further minimize the size of the images. Here is the module that helped us achieve that: cyrilwanner/next-optimized-images Automatically optimize images used in next.js projects ( jpeg, png, svg, webp and gif). github.com
This module allows you to optimize images in any Next.js project easily, and contains integrations with many different optimization packages. It also provides a very descriptive documentation on the configurations of these packages, including converting images to Webp.
Excessive DOM manipulation can easily cause our application to slow down, so it is best if we can reduce it as much as possible. Although React itself already handles the problem fairly well, we are still able to further optimize the performance of our app by minimizing the number of re-renders and DOM tree reconciliation. React provides several APIs that allows us to achieve that, including React.memo(), React.useCallback(), and React.useMemo(). By appropriately applying these methods to memoize our components, functions and state variables, we can easily prevent most of the unwanted triggers of re-renders.
Here are just a couple of tools and extensions (VS Code) that I felt extremely helpful during the process of refactoring. I believe that you may also find some of them helpful in your projects too.
You might think it’s funny, but I find it quite annoying whenever I see spelling mistakes in someone’s code. I believe there are tons of cases where bugs are caused because of spelling mistakes, and most of the time it is really hard to catch such bugs because no one’s sure how it was originally spelled.
Sometimes you might just want to see the older commits of a certain file but are too lazy to dig it on GitHub. You can easily check them very quickly inside the editor with the help of this extension.
If you are developing an app that uses i18n to manage different locales, this is the perfect extension to help you see the direct results of your locale text files. In our case, we have currently 10 different namespaces for each language we support, and it is time-consuming to check on those files whenever we need to add or modify our page contents.
This extension shows you the size of imported packages. This is especially useful to examine whether it would be appropriate to lazy-load the package or not.
The project was first initialized during the first week of June 2020, and luckily we managed to make our first deployment after one and a half month. There was an immense speed difference both on desktop computers and mobile phones. Here is a comparison of the performance score of our landing page by Google PageSpeed Insights:
Even though the new site does not seem to have a really high score, most of the performance issues actually came from 3rd party analytics trackers including Google Analytics, Crisp, Hotjar, etc. Still, that is at least 100% better than the old site!
Most importantly, it is now more friendly towards mobile users. Although most of the page traffic comes from mobile users, the actual number of people who purchased orders were greater among the desktop users. This somehow indicates that the flow of the website wasn’t friendly enough for our users to make their purchases through their mobile phones. We are excited to see if this phenomenon would change soon in the future.
There are still lots of improvements to be made, both in terms of performance and maintainability. We started to slowly shifting towards Typescript, and it already helped us on catching quite some bugs. We currently have an independent mobile app for iOS users, but not for Android users yet. Since we might not have one any time soon, it would be great to add support for PWA to our web app as soon as possible.
article written by Ping Cheng