Blog.

Next.js: Not So Straightforward Path To Production

Cover Image for Next.js: Not So Straightforward Path To Production
Andrew Zheng
Andrew Zheng

Introduction

The past few months have not been easy for anyone. I hope everyone and their families stay healthy.

To support the company’s new initiative, drive down our user acquisition costs, our engineering team was asked to come up with a solution to improve SEO (organic search result).

Ideally, we want to find a solution that supports Server-Side rendering and provides decent web performance. Out existing apps are created as a Single Page App that handles all the page rendering on the client-side and First Contentful Paint takes too long to happen due to the amount of JavaScript shipped to browsers. We decide to give Next.js a try given that it provides Server-Side Rendering out of the box and SSR should improve the page loading speed by reducing the workload in the browser. However, the path is not so straightforward. I want to share a few issues and obstacles I encounter while building our Next.js app.

There Is No Window

As developers who work with Javascript and Browsers every day, there are certain things we usually take for granted. It is in our subconscious that window object is always there when we need them. It is not the case for Next.js. window object is part of Browsers' web API instead of part of core JavaScript. For SSR, Next.js makes the initial render on the server-side in Node.js, where window object is not available.

Takeaway

For our source code that uses window or anything that comes from Web API, we will check if the object exists before calling it.

if (typeof window !== 'undefined') {
  window.addEventListener('NAV_LOADED', () => handleInitNavData());
  window.addEventListener('resize', refreshNavHeight);
}

For components we only want to render on the client-side, we can use Dynamic Import. ssr: false make sure the component is not server-side rendered.

import dynamic from 'next/dynamic';
...

const Footer = dynamic(import('../../components/ui/Footer/Footer'), {
  ssr: false,
});

Packages from node_module cannot be handled this way. Unfortunately, we either have to change the way we use the library, or we have to replace it with a new one. One example of the first situation is react-media, we had to depend on user agent from the request to provides a semi-reliable default value so it knows the default screen size.

Where Is The Environment Variable

Isn't it just process.env.MY_THING? Yes, but that's not all. Next.js provides 3 different types of environmental variables.

const globalConfig = {
  env: {
    ASSET_PATH: isDevelopment ? '' : 'https://cdn.cloudfront.net/',
  },
  publicRuntimeConfig: {
    SEGMENT_ID,
  },
  serverRuntimeConfig: {
    CACHE,
  },
};

env: It replaces the value at build time. process.env.ASSET_PATH will be replaced by https://cdn.cloudfront.net/ when we try to make a production build

publicRuntimeConfig & serverRuntimeConfig: There are runtime variables, Next.js provides an good example of them.

import getConfig from 'next/config'

// Only holds serverRuntimeConfig and publicRuntimeConfig
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig()
// Will only be available on the server-side
console.log(serverRuntimeConfig.mySecret)
// Will be available on both server-side and client-side
console.log(publicRuntimeConfig.staticFolder)

function MyImage() {
  return (
    <div>
      <img src={`${publicRuntimeConfig.staticFolder}/logo.png`} alt="logo" />
    </div>
  )
}

export default MyImage

Takeaway

Be mindful of how we use those variables and assign them to the correct places. Build time variables will not be available during runtime. Don't expose more than you need in the public runtime config. After knowing the differences between those three, common sense works best here.

No Free Cookies

I had a problem when I started working data fetching in the Next.js app. I have this code that making GET a request to get user information. However, the request failed to authenticate the user in getServerSideProps.

// Fetch user context
async function fetchUser(req: IncomingMessage): Promise<UserContext | null> {
  let userContext: UserContext | null = null;
  try {
    // our custom API helper uses fetch internally
    userContext = await ApiService.get(
      `${process.env.WEB_HOST}/web-api/user/get`,
      {}, // params
    );
  } catch (error) {
    console.error('Unable to load user context', error);
  }
  return userContext;
}

export const getServerSideProps: GetServerSideProps = async ({ req }) {
  const userCtx = await fetchUser(req);
  return {
    props: {
      userCtx,
    },
  };
}

Takeaway

We have to manually pass cookies to the request that is sent to our endpoints.

For Next.js Server:

await ApiService.get(
  `${process.env.WEB_HOST}/web-api/user/get`,
  {}, // params
  {
    // adding cookie to the new request
    cookie: req.headers.cookie,
  }
);

Other Things

  • Global stylesheet can only be added to _app.js. Next.js automatically use scoped CSS (CSS Module) for components. Next.js - Built-In CSS Support
  • assetPrefix only applied on generated assets (JS, CSS, JSON...), It has no no effect whatsoever on the assets (images, fonts...) in public folder. Next.js - CDN support
  • To organize our code better, we are able to move all our source code under src/.... Next.js - Src directory

Final Thoughts

We like Next.js a lot. It does most things right out of the box. We have already started migrating SEO focus pages into this new Next.js app, and it has been great. On the other hand, we still haven't found solutions to integrate certain Next.js features, such as Incremental Static Regeneration, into our infrastructure. If you have recently started looking into Next.js like me, I hope this post saves you some time and Googling.