Switching to Next.js and accelerating the loading of the manifold.co homepage 7.5 times

Original author: Drew Powers
  • Transfer
Today we are publishing a translation of the story about how the transition from React Boilerplate to Next.js , a framework for developing progressive web applications based on React, has accelerated the loading of the homepage of the manifold.co project 7.5 times. No other changes were made to the project, and this transition, in general, turned out to be completely invisible to other parts of the system. What turned out in the end turned out to be even better than expected.



Results Overview


In fact, we can say that the transition to Next.js gave us something like “a project productivity increase that came out of nowhere”. Here's what the project load time looks like when using various hardware resources and network connections.
Compound
CPU
To seconds
After seconds
% Improvement
Fast (200 Mbps)
Fast
1.5
0.2
750
Medium (3G)
Fast
5.6
1.1
500
Medium (3G)
Average
7.5
1.3
570
Slow (slow 3G connection)
Average
22
4
550

When using a quick connection and a device with a fast processor, the site load time fell from 1.5 s. up to 0.2 s., that is, this indicator improved 7.5 times. On a medium-quality connection and on a device with an average performance, the site load time fell from 7.5 s. up to 1.3 s

What happens after a user clicks on a URL?


In order to understand the features of the work of progressive web applications (Progressive Web App, PWA), you must first understand what happens between the moment when the user clicks on the URL (at the address of our site) and the moment when he sees something in a browser window (in this case, our React application).


Stages of work with the application

Let's consider 5 stages of work with the application, the scheme of which is given above.

  1. The user goes to the URL, the system finds out the server address using DNS and accesses the server. All this is done extremely quickly, usually taking less than 100 milliseconds, but this step takes some time, which is why it is mentioned here.
  2. Now the server returns the HTML code of the page, but the page in the browser remains empty until the resources necessary for its display are loaded (unless the resources are loaded asynchronously ). Actually, more actions are taking place at this stage than shown in the diagram, but a joint review of all these processes will also suit us.
  3. After loading the HTML code and the most important resources, the browser starts displaying what it can display, continuing to load everything else (pictures, for example) in the background. Have you ever wondered why images sometimes suddenly “pop up” on a page obviously faster than necessary, and sometimes they load too long? This is precisely why this happens. This approach allows you to quickly create a finished page.
  4. JavaScript code can be parsed and executed only after it is loaded. Depending on the size of the JS code used on the page (and this may be, for a typical React application, quite large if the code is packaged in a single file) this may take several seconds or even more (note that JS the code does not need, in order to start executing, wait for the loading of all other resources, despite the fact that on the diagram it looks exactly like that).
  5. In the case of a React application, the moment now comes when the code modifies the DOM, which causes the browser to redraw the already displayed page. Then another resource loading cycle begins. The time this step takes will depend on the complexity of the page.

The faster the better


Since a progressive web application takes React code and produces static HTML and CSS code, this means that the user sees the React application already at step 3 of the above scheme, and not at step 5. In our tests, this takes 0.2-4 seconds , which depends on the speed of the user's connection to the Internet and on his device. This is much better than the previous 1.5-22 seconds. Progressive web applications are a reliable way to deliver React applications faster to the user.

The reason why progressive web applications and related frameworks like Next.js are still not very popular is because, traditionally, JS frameworks are not particularly successful in generating static HTML code. Today, everything has changed a lot due to the fact that frameworks such as React, Vue and Angular, and others, have excellent support for server-side rendering. However, in order to use these tools, you still need a deep understanding of the features of the work of bundlers and tools for building projects. Working with all of this is not without problems.

The recent emergence of PWA frameworks such as Next.js and Gatsby (both appeared in late 2016 - early 2017) was a serious step towards widespread adoption of PWA due to lower entry barriers and due to the fact that it made using such frameworks a simple and enjoyable task.

Although not every application can be transferred to Next.js, for many React applications this transition means the same “performance out of nowhere” that we are talking about here, supplemented by an even more efficient use of network resources.

How difficult is it to migrate to Next.js?


In general, it can be noted that translating our homepage to Next.js was not very difficult. However, we encountered some difficulties that were caused by the architecture features of our application.

▍ Rejecting a React Router


We had to abandon the React router because Next.js has its own built-in router, which is better combined with optimizations regarding code separation performed on top of the PWA architecture. This allows this router to provide much faster page loading than you would expect from any client-side router.

The Next.js router is a bit of a high-speed React router, but it's still not a React router.

In practice, since we did not take advantage of the particularly advanced features that the React router offers, the transition to the Next.js router for us was to simply replace the standard React router component with the corresponding Next.js component:

/* Старый код (Маршрутизатор React) */

  A link

/* Новый код (Маршрутизатор Next.js) */

  
    A link
  

In general, everything turned out to be not so bad. We had to rename the property and add a tag for server rendering purposes. Since we also used the library styled-components, it turned out that in most instances we needed to add a property passHrefin order to ensure that the system behaves in such a way that it hrefalways points to the generated tag.


Network Requests for manifold.co

In order to see with your own eyes the Next.js router optimizations in action, open the Network tab of the browser developer tools by browsing the manifold.co page and click on some link. The previous figure shows the result of clicking on the link /services. As you can see, it leads to the execution of the download request services.jsinstead of performing a regular request.

I am not talking only about client-side routing; the React router is also suitable for solving this problem. I'm talking about a real piece of JavaScript code that has been extracted from the rest of the code and loaded on request. This is done using standard Next.js. And this is much better than what we had before. Namely, we are talking about a large package of JS-code with a size of 1.7 MB, which the client, before he could see something, had to download and process.

Although the solution presented here is not perfect, it is much closer than the previous one to the idea that users only download code for the pages they view.

▍ Features of using Redux


Continuing the topic of the difficulties associated with the transition to Next.js, it can be noted that all the interesting optimizations that Next.js undergoes the application have a certain impact on this application. Namely, since Next.js performs code separation at the page level, it does not allow the developer to access the root component Reactor the render()library method react-dom. If you have already been involved in configuring Redux, then you can note that all this tells us that for normal operation with Redux we need to solve the problem, which is that it is not clear exactly where to look for Redux.

Next.js provides a special higher-order component, withReduxwhich acts as a wrapper for all top-level components on each page:

export default withRedux(HomePage);

Although all this is not so bad, but if you need methods createStore(), such as when using redux-reducer-injectors , expect that you need additional time to debug the wrapper (and, by the way, try to never use anything something like redux-reducer-injectors).

In addition, due to the fact that Redux is now a “black box”, using the Immutable library with it becomes problematic. Although the fact that Immutable will work with Redux seems quite obvious, I ran into a problem. So, either the top-level state was not immutable (error get is not a function), or the wrapper component tried to use dot notation to work with JS objects instead of the method .get()(errorCan’t get catalog of undefined) To debug this problem, I had to refer to the source code. After all, Next.js forces the developer to use its own mechanisms for a reason.

In general, it can be noted that the main problem associated with Next.js is that very little in this framework is well documented. There are many examples in the documentation on the basis of which you can create something of your own, but if among them there is not one that reflects the features of your project, you can only wish good luck.

▍Fetch rejection


We used the react-inlinesvg library , which offers styling options for embedded SVG images and query caching. But here we had one problem: when performing server rendering, there is no such thing as XHR requests (at least not in the sense of URLs generated by Webpack, as you might expect). Attempts to execute such requests interfere with server rendering.

Although there are other libraries for working with embedded SVG data that support SSR, I decided to abandon this feature, since SVG files were still rarely used. I either replaced them with regular images, tags, in the event that when the output of the corresponding images did not need stylization, or built them into the code in the form of React JSX. Probably, everything just got better, since the JSX illustrations now hit the browser when the page was first loaded, and the JS bundle sent to the client had 1 less library.

If you need to use data loading mechanisms (I needed this feature for another library), then you can configure it using next.config.js, using whatwg-fetchand node-fetch:

module.exports = {
  webpack: (config, options) =>
    Object.assign(config, {
      plugins: config.plugins.concat([
        new webpack.ProvidePlugin(
          config.isServer
            ? {}
            : { fetch: 'imports-loader?this=>global!exports-loader?global.fetch!whatwg-fetch' }
        ),
      ]),
    resolve: Object.assign(config.resolve, {
      alias: Object.assign(
        config.resolve.alias,
        config.isServer ? {} : { fetch: 'node-fetch' }
      ),
    }),
  }),
};

▍ Client and server JS


The last feature of Next.js, which I would like to mention here, is that this framework is launched twice - once for the server, and again for the client. This slightly blurs the line between client-side JavaScript and Node.js code in the same code base, causing unusual errors, like fs is undefinedtrying to take advantage of Node.js features on the client.

As a result, we have to construct such structures in next.js.config:

module.exports = {
  webpack: (config, options) =>
    Object.assign(config, {
      node: config.isServer ? undefined : { fs: 'empty' },
    }),
};

A flag config.isServerin Webpack will be your best friend if you need to run the same code in different environments.

In addition, Next.js supports, in addition to the standard methods for the life cycle of React components, a method getInitialProps()that is called only when the code runs in server mode:

class HomePage extends React.Component {
  static getInitialProps() {
    // Это вызывается только при первом проходе серверного рендеринга
  }
  componentDidMount() {
    // Это вызывается только на клиенте, при монтировании компонента
  }
  …
}

Yes, and let's not forget that our good friend, an object windownecessary for organizing listening to events, for determining the size of the browser window and giving access to many useful functions, is not available in Node.js:

if (typeof window !== 'undefined') {
  // Пожалуйста, позволь мне работать с `window` не устроив тут полный беспорядок
}

It should be noted that even Next.js is not able to save the developer from the need to solve problems associated with the execution of the same code on the server and on the client. But in solving such problems are very useful config.isServerand getInitialProps().

Results: what will happen after Next.js?


In the short term, the Next.js framework perfectly matches, in terms of performance, our requirements for server rendering and the ability to view our site on devices that have JavaScript disabled. In addition, now it allows you to use advanced (rich) meta tags .

Perhaps in the future we will consider other options in the event that our application needs both server rendering and more complex server logic (for example, we look at the possibility of implementing single sign-on technology at manifold.co and dashboard.manifold.co ) But until then we will use Next.js, since this framework, with small time costs, brought us huge benefits.

Dear readers! Do you use Next.js in your projects?


Also popular now: