Introduction
Recently, I was building my portfolio site using React and the built-in create-react-app toolchain. The webpage worked well and I was able to deploy it on GitHub Pages with ease. However, create-react-app uses client-side rendering, which causes an obvious layout shift of the components when the page is first loaded or refreshed. Not a big deal, but it’s annoying😠.
Since the webpage doesn’t change often, the best solution is to utilize static site generation (SSG), which pre-renders the page and thus eliminate the layout shift. Unfortunately, create-react-app doesn’t provide the functionality out of the box, and although there are some third-party packages such as react-snap that do the job, they are outdated and don’t support the latest React versions.
Therefore, I decided to migrate to Next.js, a popular React framework for building web applications with server-side rendering and static site generation functionalities. The migration process is quite simple, and in this post I will guide you through how I did it, using my own site as example.
Getting Started
The complete code changes could be found in resources.
Migrate to Next.js
First, uninstall react-scripts and install Next.js:
1 2
npm uninstall react-scripts npm install next
Then, replace the scripts in
package.json
from react-scripts:1 2 3 4 5 6 7
// package.json { ❌ "scripts": { "start": "react-scripts start", "build": "react-scripts build" } }
to Next.js:
1 2 3 4 5 6 7 8 9
// package.json { "scripts": { "start": "next start", "build": "next build", "export": "next export", "dev": "next dev" } }
Then, create a folder named
pages
in the root directory, and add a file named_document.js
with the following content:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/* pages/_document.js */ import { Html, Head, Main, NextScript } from "next/document"; export default function Document() { return ( <Html> <Head> <link rel="icon" href="/favicon.ico?" /> <meta name="description" content="Chin Hang's Portfolio" /> <link rel="apple-touch-icon" href="/logo192.png" /> <link rel="manifest" href="/manifest.json" /> </Head> <body> <Main /> <NextScript /> </body> </Html> ); }
The content in
<Head>
was copied from that of<head>
inpublic/index.html
. Note that we remove the%PUBLIC_URL%
from the href:1 2
<!-- public/index.html --> <link rel="icon" href="%PUBLIC_URL%/favicon.ico?" />
to:
1 2
/* pages/_document.js */ <link rel="icon" href="/favicon.ico?" />
In short, this document is where we add the
<head>
code that are common to all pages, which Next.js will later pick up for page rendering. Also note that some other metadata such as title, viewport, and charSet are not placed here but rather inpages/_app.js
, which will be created later. The<Main />
and<NextScript />
components are needed by Next.js and are not of our concern.Inside the same
pages
folder, create another file named_app.js
with the following content:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
/* pages/_app.js */ import Head from "next/head"; export default function MyApp({ Component, pageProps }) { return ( <> <Head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Chin Hang's Portfolio</title> </Head> <Component {...pageProps} /> </> ); }
In short, this document is the starting point where all the pages are initialized, and thus is useful for making changes that are common to all pages such as adding global css. Here, we put the rest of the metadata such as viewport and title as recommended by Next.js. I put the
<title>
here because my site is a single-page application and thus all pages share the same title.We can now delete
public/index.html
since it’s not used anymore.Rename the
src
folder tocomponents
, which is the convention used in Next.js.Then, we create another file named
index.js
inside thepages
folder with the following content:1 2 3 4 5 6
/* pages/index.js */ import App from "../components/App"; export default function HomePage() { return (<App />) }
This file is the entry point to our home page, so I return my
App
here, which contains my home page elements. The content in this file should be similar to what the originalsrc/index.js
was doing, so if you have made any changes to it you should probably adapt it here.We can now delete
components/index.js
(previouslysrc/index.js
) since it’s not used anymore.Add the
out
and.next
folders used by Next.js for output to.gitignore
(if exists):1 2 3 4 5 6 7
# .gitignore ... /out .next ...
Now, we can run our application and it should work the same as before:
1
npm run dev
If you want to generate the static assets for deployment, run:
1 2
npm run build npm run export
and the generated files will be placed in
out/
folder.
That’s it! We have successfully migrated to Next.js🎉. Now it’s time to deploy our static site to production, which is GitHub Pages in our case.
Deploy to GitHub Pages
To streamline the deployment process, we make use of GitHub Action to generate the static assets and deploy it to GitHub Pages whenever the code is pushed to main. Create a file .github/workflows/gh-pages.yml
with the following content:
|
|
Note that in the “build static pages” stage, we add a .nojekyll
file into the out/
folder generated by build and export. This is because next export
puts some static files in out/_next
, and by default GitHub Pages ignores underscore-prefixed directories. Therefore, we add a .nojekyll
file in the same level as the _next
directory to avoid it being ignored. Then, in the “deploy” stage, we specify out/
as our folder so that its content will be copied to the root of our gh-pages
branch.
The last step is to change the settings of our repo to serve the content from root of gh-pages
branch as follows:
Also, make sure you set the Settings > Actions > General > Workflow permissions
to “Read and write permissions” so that GitHub Action is allowed to help us deploy.
Now, every time we push our code to the main
branch of our repository, our website will be deployed automatically👏.
More About the Migration
The above migration steps work perfectly for my single-page portfolio site, but there could be many more configurations to do depending on how complex your site is. For example, if your site has multiple routes or has problems resolving image url in production, there may need to be more changes such as adding environment variables. Read more on the official guide if you are having issues.
Extra: Faster Font Loading
Although the layout shift due to client-side rendering is now solved with static site generation, there is still one thing that bugs me — the font. Because I’m using Google Font, there is some delay in fetching the font from Google’s CDN, which causes the browser to use the default font before switching to my font only when it’s fully fetched. That causes a visible font/layout change to my landing page as well😠.
Luckily, Next.js provides built-in support for automatic self-hosting for any font file. This means we can load web fonts much faster and minimizes the visible effect of layout shift. To do so, we just need to:
- Import the
next/font
package in our code, - Declare the font we want, and
- Use it to set our font family.
To demonstrate, for my site, I changed from using Material UI (MUI) fontsource:
|
|
to next/font
:
|
|
By simply using the font package provided by Next.js, our font will be automatically self-hosted, which improves the load time by multiple magnitudes.
Summary
We have gone through how to migrate our React application from create-react-app to Next.js so that we could do static site generation and speed up our website. We have also learned how to deploy our static site to GitHub Pages, as well as how to utilize font package from Next.js for faster font loading. If you are facing similar problem as me, the migration is well worth it.
Resources
See the code changes in effect: https://github.com/CookieHoodie/cookiehoodie.github.io/compare/8442a7e57a350b1e9981eeb29be239a600e4b6f0...417d928e1e84730475cbc387d96bb3bb2cea9985