(source)


Introduction

  • When performing a server-side render of a next.js React page component, some server-only code may be required (eg. fetch data from a in-memory server cache, server logging)
  • However, some server-only code:
    1. Should not be executed on the client
      • eg. bundling an in-memory XHR cache that was only meant to be used on the server-side
    2. Cannot be executed on the client
      • eg. emitting server logs via hot-shots, which requires a dgram dependency that does not exist on the client bundle.
  • Thus, isolating next.js server-only modules helps to ensure that we avoid bundling server-only code into our client bundles.
  • There are two general 2 approaches to do this:
    1. Isolate via calling server-only code within getServerSideProps()
    2. Isolate via module import alias

Scenario

Given the following setup:

  1. A page is hosted on a next.js instance
  2. Page needs to make 1x XHR call as part of its server-side rendering step
  3. XHR call may be cached by using a memcached instance

Goal: We want to ensure that all memcached-related code is not included in the next.js client bundle.

Note: Refer to my example repo nextjs-ssr-localcache-example for code and documentation references.

Approach 1: Isolate via calling server-only code within getServerSideProps()

This is the recommended way to isolate server-only modules.

  • Given that the server-only code is called only within getServerSideProps(),
  • When the client bundle build step is performed,
  • Then next.js will perform tree-shaking for getServerSideProps(), thus ensuring that server-only code will neither be bundled nor invoked on the client side.

Note: This also works for getStaticProps() as well.

Steps:

  1. Invoke server-only code in getServerSideProps()

    // UserCachedSspPage.tsx
    const User = (props) => { ... };
    
    export async function getServerSideProps() {
       const xhrCacheHelper = makeXhrCacheHelper();
       if (xhrCacheHelper) {
          const { getKey } = xhrCacheHelper;
          const dataString = (await getKey("userData")) as string;
          ...
       }
       ...
    }
    

    (Code)

Approach 2: Isolate via module import alias

However, if you’re in either of these scenarios:

  1. You are on a version of next.js <9.3, where getServerSideProps() is not available
    • eg. executing server-only code within getInitialProps()
  2. You want to execute some server-side code within React components that are invoked during a server-side page render
    • eg. Collecting performance metrics and emitting logs on server-side only

…then you would need to manually isolate server-only code.

Steps:

  1. Organize server-only code in a separate folder (eg. src/SERVER_ONY_MODULES)

    // src/SERVER_ONLY_MODULES/xhrCacheHelper/xhrCacheHelper.ts
    import Memcached from "memcached";
    
    export const makeXhrCacheHelper = () => {
      const memcached = new Memcached("...");
    
      const closeConnection = () => { ... };
      const setKey = (key, value) => { ... };
      const getKey =  (key) => { ... };
      return { setKey, getKey, closeConnection };
    };
    

    (Code)

    xhrCacheHelper() defines an object with functions to get/set values with the memcached instance.

  2. Access server-only code using import aliases (eg. @SERVER_ONLY_MODULES)

    // UserCachedIpPage.tsx
    const getMakeXhrCacheHelper = async () => {
      const module = await import("@SERVER_ONLY_MODULES");
      if (!module) {
        return noop;
      }
      return module.makeXhrCacheHelper;
    };
    
    const User = (props) => { ... };
    
    User.getInitialProps = async () => {
       const makeXhrCacheHelper = await getMakeXhrCacheHelper();
       ...
    }
    

    (Code)

    getMakeXhrCacheHelper() is a utility that defensively checks for the existence of the module; this is necessary as the import() statement would not resolve to the actual module (ie. return undefined) when executed on the client browser.

  3. Isolate server-only module imports for client bundle builds (via webpack config override via next.config.js)

    // next.config.js
    const webpackConfigFn = (config, { isServer }) => {
      // for webpack to resolve import alias correctly
      config.resolve.alias["@SERVER_ONLY_MODULES"] = "src/SERVER_ONLY_MODULES";
    
      if (!isServer) {
        // resolve @SERVER_ONLY_MODULES as empty module on client
        config.resolve.alias["@SERVER_ONLY_MODULES"] = false;
      }
    
      return config;
    };
    

    (Code)

    We provide configuration to support the @SERVER_ONLY_MODULES import alias, as well as to resolve @SERVER_ONLY_MODULES to an empty module on the client browser.

Appendix

This article is inspired by Arunoda Susiripala’s article, SSR and Server Only Modules (referenced by official next.js documentation).

However, I found that I was still getting bundle build errors and client bundle runtime errors - This article fleshes out an alternative approach to isolating server-only modules.

Likewise as with Arunoda’s article, I hope this article improves the developer community’s awareness of this issue 🙃