Dynamic imports and preloading in Next.js, doing it the right way

Last Updated Sep 5, 2024

Written By

Image of laptop showing on coding mode

Next.js is a powerful React framework for building web applications. Next.js allows for hybrid Static Site Generation, SSG, and Server-Side Rendering, SSR.

Another cool feature of Next.js is its support for Incremental Static Regeneration. With Incremental Static Regeneration, you can add, update, and delete statically rendered pages after building time without redeploying the whole application.

Incremental static regeneration is one big difference and advantage that Next.js has over Gatsby jsanother popular React framework. With incremental static regeneration, your web application can scale to thousands or millions of pages while build-time stays the same.

Next.JS has other benefits, such as internationalization and many others. This article aims to show how to optimize Next.js web applications by preloading critical Javascript modules while deferring the loading of non-critical modules.

Critical Javascript bundles refer to Javascript files needed at an early stage of running a web application. In other words, they are JavaScript files that affect any of the six core web vital metrics.

Preloading of web assets: how it works

Web assets preloading is a way of speeding up the download of critical web asset files. If done well, it can reduce the page's load time and potentially improve the page's Time to Interactive TTI and other core web vital metrics such as Largest Contentful Paint.

Pre-loadable asset files do not necessarily have to be Javascript files; they could be font files, media files, CSS files, JSON files, Iframe embeddable documents, and many others.

Asset preloading works using the HTML link element with its rel attribute set to preload. Browsers start downloading preloaded assets in a separate thread while it continues to parse and render the webpage.

Below's code snippet exemplifies how web asset preloading works in a bare HTML file using the link element.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Document</title>

    <!-- some preload examples -->
    <link rel="preload" href="/js/bundle.js" as="script" />
    <link rel="preload" href="/images/hero-image.png" as="image" />
    <link rel="preload" href="/fonts/typeface.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
    <link rel="preload" href="/json/config.json" as="fetch" />
    <link rel="preload" href="/css/styles.css" as="style" />
  </head>
  <body>
    <h1>How html link preload works</h1>
  </body>
</html>

This way, the preloaded assets would become available even before they are needed. This has potential benefits and, when coupled with the HTTP2 protocol, can greatly increase page load time and time to responsiveness.

To preload a web resource, you use the link element and set its rel attribute equal to preload. You must also set an as attribute to indicate how the preloaded resource is to be handled. Browsers ignore preload directives without an appropriate as attribute.

Webpack's handling of dynamic imports

Dynamic import statements in Next.js works similarly to every other dynamic import in other frameworks, with some subtle improvements made by the Next.js team.

Dynamic imports are processed by Webpack and are the recommended way of doing code-splitting. Code-splitting is a way of breaking down your javascript bundles into smaller chunks that can be loaded on-demand or parallel to speed up their download time.

At the heart of Webpack's code-splitting is the Webpack's Split Chunks Plugin.

When Webpack finds an import statement during the build phase, it creates a separate chunk or merges the chunk into an already existing chunk depending on the code-splitting rules defined in a Webpack configuration file.

/**
 * the webpackPreload:true annotation causes webpack to treat
 * this chunk as a critical resource to be preloaded
 */
import(/* webpackPreload: true */ '../IconPack').then(result => result.default);

By default, dynamic import statements are lazy-loaded or loaded on demand by Webpack but can be preloaded if desired by surrounding the import statement with the webpackPreload: true annotation as shown in the code snippet above.

Note:

Webpack's preload directive/annonation does not work in Next.js. Next.js bundle preloading works through its next/dynamic package.

Specifying dynamic chunk filename for Webpack

By default, Webpack computes the chunk's file name using its content hash. However, you can specify a file name for the dynamically imported chunk by surrounding the import statement with a webpackChunkName: "filename" annotation as shown below.

/**
 * the webpackChunkName: "name" annotation defines the filename for the generated chunk
 */
import(/* webpackChunkName: "icon-pack" */ '../IconPack').then(result => result.default);

The webpackChunkName annotation works in Next.js unlike the preload annotation.

Dynamic imports in Next.js

To fully understand how we can preload dynamic imports in Next.js, we will split dynamic imports in Next.js into two import categories or types:

  1. Dynamically importing React component
  2. Dynamically importing any other files

React component imports are imports that dynamically load react-renderable components. These components are imported on a component's mount or some user actions.

The Next.js team created the next/dynamic package to make it easy to import React modules dynamically. In addition, the next/dynamic package behaves similarly to loadable components, a package used to optimize resource loading in create-react-app applications.

import dynamic from 'next/dynamic';
import { FunctionComponent } from 'react';

const ChatWidget = dynamic(() => {
  return import('../widgets/Chat').then((result) => result.default);
});

const CommentWidget = dynamic(() => {
  return import('../widgets/Comment').then((result) => result.default);
});

export const WidgetLoaders: FunctionComponent<{
  widgetType: 'chat' | 'comment';
  [p: string]: any;
}> = ({ widgetType, ...props }) => {
  switch (widgetType) {
    case 'chat':
      return <ChatWidget {...props} />;

    default:
      return <CommentWidget {...props} />;
  }
};

Next.js runs two kinds of builds when the next build command is executed:

  1. Server build
  2. Client build

When used, the next/dynamic package can be configured to render only on the client. This is achieved by passing an options object as a second argument to the dynamic method with an ssr property set to false.

Client-only dynamic imports (i.e., imports that have the ssr option set to false) will not render during server-side rendering and will not be preloaded by Next.js; neither will it be included in the application's critical bundles.

const ChatWidget = dynamic(() => {
  return import('../widgets/Chat').then((result) => result.default);
}, {ssr: false});

Next.js preloads every dynamically imported module made using the next/dynamic package that renders on the server.

One common mistake that web developers make while using the next/dynamic package is not setting the ssr option to false when importing client-only intended modules.

Preloading of dynamically imported non-react Next.js modules

Next.js does not support Webpack's webpackPreload annotation, leaving us with the next/dynamic package as the only resort to getting dynamic imports preloaded in Next.js projects.

However, the next/dynamic package was created by the Next.js team specifically for dynamic React module imports. This limitation brings the need for a little hack to get other dynamic file imports in Next.js preloaded.

The solution is to use the next/dynamic package, import the required file, but return an empty React component. Basically, a component that renders null.

import dynamic from 'next/dynamic';
// we create an empty react module
const ServiceWorkerPreloader = dynamic(() => {
  return import(
    /* webpackChunkName: "sw" */ '/sw.js'
  ).then(() => {
    return () => null;
  });
});

The hack works by importing the desired non-react module file but returning an empty React function. The last hack is to mount the component in your React tree, as shown below:

export const MyComponent = ({ widgetType, ...props }) => {
  useLayoutEffect(() => {
    import('/sw.js').then((result) => {
      conse sw = result.default;
      // register service worker
    });
  }, []);

  return (
    <>
     <ServiceWorkerPreloader />
    </>;
  );
};

Using the hack above, you can preload dynamically imported non-react modules in Next.js.

Conclusion

Next.js does not support Webpack's webpackPreload annotation for preloading dynamic imports. Therefore, to get dynamic imports preloaded in Next, they must pass through the next/dynamic package.

The next/dynamic was created by the Next.js team specifically for importing React modules. But using the hack shown in this article, you can get non-react modules to work similarly.