[Note from Realize Me] We’re very excited to release the first post in our engineering series. We are a team of experienced technologists and are building a robust, highly secure, and performant platform with tools that we’ve done extensive research on, and we’re thrilled to take a deeper dive into our architecture with you. Today’s post comes to you from Brendan Morrell, our lead frontend engineer.
As the web has evolved, the usage and scope of javascript have exploded as a means to build increasingly complex applications on both the client and server. In this rapidly changing environment, there has been an influx of tools aimed at helping developers manage this complexity, with new frameworks emerging and being replaced constantly. Once an organization has chosen a technology, switching is expensive, time-consuming, and so cumbersome that it is frequently neglected to the detriment of the product. Thus, choosing appropriate tools that satisfy the product requirements, promote rapid development and iteration, and will continue to grow and mature to meet the needs of the future is critical for the success of an organization.
With this in mind, when we set out to build Realize Me, we chose tools that would allow us to best achieve the goals of the platform within the constraints of our organization. The starting point was the decision to build the app in React. Realize Me is a health and performance optimization platform with complex interactional requirements, so a powerful and established UI library is a must. At this point, React is so well-established that I doubt I need to give too much justification, suffice it to say that React is an unopinionated, declarative library that makes it very easy to build clean, fast, maintainable user interfaces; it has a gigantic community surrounding it and actively maintaining the ecosystem, and it is only getting bigger. Here is a screenshot of NPM trends for React, Angular, Vue, and Svelte.
As you can see, in terms of usage, the others don’t hold a candle to React. Moreover, not only is this trend not slowing, the rate of adoption is actually still increasing, so you can be fairly confident that it will continue to be a viable option with plenty of developers long into the future. Add to that the fact that you can get pretty far on both iOS and Android with React Native (a feature that is especially attractive for startups like Realize Me that may not have the time nor resources to create three entirely independent engineering teams for each platform), and React is an obvious choice.
Once we had settled on React, the options for how to build the application narrowed considerably. The first question is whether to use a framework at all or to simply build the entire application from scratch simply using React as the UI layer. This is by far the most flexible option available. However, with flexibility also comes overhead, and having to handle things like module bundling and compilation, routing, server-side rendering, code splitting, and a multitude of other complex tasks on your own quickly becomes unsustainable (or at the very least, unnecessarily onerous), and it is incredibly difficult to do well. For this reason, the vast majority of production applications opt to use a framework so a lot of this low-level work is done for you and handled with meticulous precision.
Because we (1) have no desire to reinvent the wheel for all of these tasks, and (2) are quite confident that any attempts we would make to solve these problems would fall far short of dedicated teams of the brightest engineers in the world, we knew we wanted a framework to do some of the heavy lifting. Within the React ecosystem, the three most popular frameworks are Next.js, Gatsby, and create-react-app (CRA). A promising newcomer on the scene is Remix, created by Ryan Florence and Michael Jackson from React Router. However, we ruled Remix out solely based on the fact that it is so new (they finished their beta within the last few months) that it has not been battle-tested enough to stake a production application’s future on it. The main differences between the remaining frameworks are the way and degree to which the React UI is constructed and available prior to loading in the browser, as well as various bundled helpers and optimizations to make the code more performant and improve the developer experience.
As a refresher, the content visible on a web page is constructed out of HTML elements styled with CSS. Javascript is the programming language that allows interaction within this structure, and which can be used to dynamically alter the content on the page. In traditional web pages, 100% of the content already exists, and javascript essentially just handles simple things like sending and receiving data for things like form submissions. In single-page applications (SPAs), that paradigm is flipped. None of the content on the page is built into the application initially, and what the browser receives instead is a huge chunk of javascript code, which, after loading, executes and creates the content on the page. Because javascript is dynamic, this not only allows for incredibly interactive, customized, and fluid content on each page but further allows for instant navigation between pages; the content for the next page can be created on the fly by the javascript code without the need for a full page refresh while waiting on a request for new HTML from the server. This speed and dynamism come at a cost, though. Most obviously, because the page content is populated with javascript only after the js bundle has been executed on the client, the initial page load is a blank screen. This is not only jarring for a user due to the significant layout shift and lack of interactivity while the content is injected into the page, but is devastating for SEO as many crawlers do not run javascript, and thus see nothing on the page. The second issue this causes is page load times at scale. If one of the goals of a single-page application is instantaneous navigation between pages, then the code responsible for pages beyond the current page must already be loaded in the browser. If I navigate from page A to page B, the code to create page B must be on page A, and thus when I load page A, the server needs to send the code for both page A and for page B. Extend this example to a site with thousands of pages, and you can imagine the problem.
I know from experience that solving these two issues well is incredibly difficult. Thus, CRA – which aims to provide a tool for rapidly bootstrapping React apps while leaving the implementation of the server, routing, server-side rendering, lazy loading and code splitting entirely up to the developer – was off the table.
That left Next.js and Gatsby. On paper, both frameworks share a similar feature set, have an excellent developer experience, and produce lightning-fast built applications. However, a look at their origins and underlying philosophies gives insight into which use cases each is best suited.
Gatsby was created on the idea that most of the issues facing react applications could be solved by fetching the vast majority of app data at build time, statically generating the content for every page in an application (static site generation, SSG), and then serving these pages as static files which are then hydrated with only the javascript required for user interaction and for lazy loading any code required for subsequent navigation. If the page has dynamic content, which cannot be reliably fetched at build time, the recommended approach is to make skeleton pages that share the same layout as the final page, build these, and then serve this with the required javascript to make any data requests client side and update the page. By doing this, Gatsby generates incredibly lean, performant, pages split into very basic static files which can be cached easily and served from a CDN without even requiring one's own server for hosting. The downside to this is that pages with dynamic content (think e-commerce or social networking sites) end up having to either serve stale content or rely on skeletons that are only filled in on the client, and build times can get prohibitively long as the number of pages increases. For this reason, Gatsby is mostly used for building websites where the number of pages is predictable, and the content remains mostly static.
Next.js started with a server first approach in which each time a page is requested, the server handles the data fetching on the fly, and then uses that data to render the react tree on the server (server-side rendering, SSR) and send back the fully formed HTML with the corresponding javascript to hydrate the page after the initial page load. It also provides a rich API for page-based routing with complex dynamic routing structures, allowing for the possibility of an infinite number of indeterminate pages which are all fully formed on page load with up to date data. The drawback to this is that each request now needs to hit the server directly, and the response will only be able to be sent after the data has been fetched and the server has rendered the react tree into HTML.
As time has progressed, both frameworks have added features to combat their own shortfalls and have adopted popular elements of the other. As it stands, each is now capable of both SSR and SSG, and they have both even added techniques to enhance SSG by only pre-generating a subset of the pages and allowing individual pages to be selectively regenerated as new data becomes available (incremental static regeneration, ISR). However, functionally speaking, the SSR capabilities and dynamic routing within Next.js are significantly more optimized, performant, and extensible, and its SSG and ISR abilities are now on par with Gatsby (in fact, SSG is now the Next.js team’s recommendation for most use cases, reserving SSR only for when truly necessary). Additionally, because Next.js was designed from the start to be a server-centric framework, it not only does not restrict what you can do server-side, it even provides a vast amount of server-specific utilities and helpers giving you much greater control over how your application runs and what it can do.
Bearing this in mind, although both are suitable candidates in terms of core functionality, Next.js is better suited to handle the dynamic data and complex requirements we have. Realize Me is primarily concerned with real-time data monitoring and altering, which Next.js handles flawlessly. We also rely on a significant amount of bi-directional server-client communication with graphql subscriptions, sockets, and a number of custom API services which would be difficult if not impossible within the confines of the Gatsby framework without creating an entirely separate service. Beyond this, we are developing a subset of social networking functionalities that will increase these demands even further. In short, Next.js gives us the best of both worlds: lightning fast, cache-able, pre-generated static content, as well as robust server functionality with pre-optimized, developer-friendly utilities baked in, and escape hatches to customize as desired.
The final reason we chose Next.js is less quantifiable and is instead more of an accumulation of experiences and appraisals of a multitude of different features. Basically, after using both, and observing countless others experiment with each framework, we have found Next.js to be a much more robust, extensible, and scalable framework throughout the full spectrum of possible requirements you might encounter in app development. At times, this comes at the price of needing to write code in the “Next.js” way, but even if mildly inconvenient or requiring a slightly steeper learning curve, the Next.js ecosystem, opinionated as it is, is solid, and when you stay within it and follow the rules, your application benefits. This is clearly a generalization, but when you look at the data, they corroborate it. Gatsby doesn’t last long at scale. Far more of the world’s largest companies vote with their feet in support of Next.js over any other framework, including Uber, Netflix, Starbucks, Nike, Target, and countless others. Similarly, Next.js stands at over ninety thousand GitHub stars as opposed to Gatsby’s fifty, and the npm trends show that Next.js has been pulling ahead of Gatsby at a staggering rate in the past two years.
In the end, the “correct” choice for any project depends on the requirements. For small projects with no immediate need for capabilities beyond the client, and where development speed is critical, CRA is a great choice. As your application grows and speed and/or SEO become important, upgrading to a framework like Next.js or Gatsby with performance baked into it becomes a necessary upgrade. And for us at Realize Me, given the scale and breadth of tasks required of the application, and the need for real-time, updatable content with minimal performance sacrifices, Next.js has been a fantastic choice.