Starting a new project with all the latest tools is exciting. But as companies grow, tech stacks can become outdated and inefficient. Here at Dialectica, we prioritize quality and performance so keeping our tech stack up to date is one of our biggest priorities. In this post, we’ll share our journey of modernizing our tools to deliver a better user and developer experience.
Driving Innovation: Our Goals for a Modern Tech Stack
In today’s fast paced competitive market, speed and quality are two of our core strengths at Dialectica and it’s also what differentiates us from the competition. So enabling our users and developers to be more efficient in their day to day is very important.
Our aim for these upgrades was twofold.
User Experience
Our goal is to deliver a seamless and efficient user experience by significantly reducing page load times and improving overall application performance in order to maximize productivity and user satisfaction.
Developer Experience
Our objective is to create a more efficient and enjoyable development environment by optimizing build times, reducing page reload times, and streamlining the overall workflow. No more waiting around for slow page reloads during development!
Charting Our Course: Planning and Identifying Challenges
Our initial goal was to upgrade to the latest version of React, which at first seemed straightforward. To initiate the upgrade process, we developed a comprehensive plan and divided it into smaller, more manageable tasks in order to tackle them more efficiently. However, we soon encountered significant challenges arising from complex interdependencies between libraries and a poorly managed lockfile.
The Root of the Problem: Yarn v1 and Its Nested Lockfile structure
After some debugging, we came to the conclusion that the primary culprit was our reliance on Yarn v1. Its nested lockfile structure, while once efficient, had become a source of issues. This hierarchical organization of dependencies and their versions often led to complex and difficult-to-read lockfiles. More importantly, it could result in incorrect dependency resolution, causing various errors and conflicts, which in our case was the issue.To better visualize the issue:
Project A directly depends on Package B.
Package B depends on two different versions of Package C.
A((Project)) — Requires –> B((Package B))
B — Requires –> Cv1((Package C v1))
B — Requires –> Cv2((Package C v2))
This can lead to conflicts and inconsistencies, as different parts of the project might rely on different versions of the same package.
From Nested Nightmare to Our Flat Savior: Yarn v4
Yarn v4, on the other hand, introduced a flat structure for the lockfile. This means that all dependencies and their versions are listed in a single flat list, regardless of their level in the dependency tree. This makes the lockfile easier to read and understand, but it also supports deduplication of the lockfile natively. This improves the efficiency and performance of the project by reducing redundancy in the dependency tree by eliminating duplicate dependencies.
From Uncertainty to Clarity: Overcoming the Challenges
After identifying the issue, we had a clear path forward. We started by carefully studying Yarn’s documentation, and we were soon able to successfully complete the upgrade, paving the way for our React migration. Then it was a matter of simply following the official React migration guide, and updating our codebase to align with the new version requirements. Being able to run our app locally again was a testament to our hard work and gave us immense satisfaction. But our journey was far from over.
Optimizing for Performance
In addition to the major React upgrade, we seized the opportunity to optimize our project further. We upgraded key libraries like Jest, React Testing Library, ESLint, Webpack, and Webpack Dev Server to their latest versions. These upgrades brought significant performance improvements and enhanced developer experience.
Furthermore, to further reduce our bundle size and improve load times, we implemented lazy loading for our components. This technique dynamically loads components only when they are needed, resulting in faster page loads and a more responsive user experience.
A monumental Merge Request
The culmination of our efforts was a massive merge request that encapsulated the entirety of our upgrade journey. It was a testament to the hard work, dedication, and technical expertise of our team.
Testing and Quality Assurance: The Key to a Successful Transition
To ensure a seamless transition, we adopted a rigorous testing strategy. Our commitment to code quality and comprehensive test coverage allowed us to identify and address potential issues early in the process.
By breaking down the testing tasks into smaller, manageable chunks, we optimized our workflow and maintained efficiency. A code freeze was implemented to minimize disruptions and focus on the testing phase. Additionally, we allocated sufficient resources to ensure timely completion without compromising user experience or delaying new features.
While most issues were resolved promptly, we encountered a few perplexing problems. As it turned out, our initial fixes were inadvertently causing the issues. This experience reminded me of the wise words of Filipe Fortes
Doubling Down on Performance: Post-Upgrade Improvements
Once we had successfully completed the upgrade and rigorous testing, it was time to quantify the benefits we had achieved.
User Experience
By strategically implementing lazy loading for our routes and optimizing our package management, we achieved a remarkable 61% reduction in our application’s bundle size. This substantial decrease directly translates to significant performance gains:
- Faster Load Times: A smaller bundle size means quicker downloads, reducing initial load times and improving user experience.
- Enhanced Performance: Lower memory consumption and optimized resource utilization lead to smoother interactions and fewer performance bottlenecks.
- Reduced Server Load: Smaller bundles place less strain on servers, contributing to overall system efficiency.
Coupled with the latest advancements in React, which introduced significant optimizations like reduced page re-renders, we further elevated our application’s performance.
Leveraging Lighthouse analytics, we quantified these improvements, observing an impressive 49% increase in overall performance. In practical terms, this means most pages now load approximately two times faster than before.
From:
To:
Developer Experience
Finally, the transition to React 18 and the latest versions of our core libraries has significantly enhanced developer experience and efficiency.
Yarn
A pivotal upgrade was to the latest version of Yarn. This streamlined dependency installation now saves us approximately 2 minutes per installation and introduced valuable features like dependency deduplication, which was crucial for this and future upgrades.
From:
Down to:
Webpack
Combined with React, the updates to Webpack have significantly accelerated local development. Uncached builds now start twice as fast, while cached builds start nearly three times faster. The most notable improvement, however in my opinion, is in page reload times. Previously a 10-second wait, has now been reduced to a mere second, a 10x acceleration in development feedback loops.
Dev server start from:
Down to:
Page Reload from:
Down to:
To summarize the improvements in a chart:
Conclusion
By embracing the latest tools and technologies, we have successfully modernized our tech stack, significantly enhancing both user and developer experiences. This journey, while challenging, has proven to be invaluable, yielding tangible benefits in terms of performance, efficiency, and maintainability.
As we continue to evolve, we remain committed to prioritizing quality and innovation. By staying ahead of the curve, we can consistently deliver exceptional products and services that exceed expectations.