Things I've Learned Building Next.js Apps



I've spent a lot of time in the past 4 months creating Next.js apps for both work and personal use.

Along that journey, I've grown to really ❀ Next.js and it's ecosystem. Here are some of the things I've learned along the way.


I used to be strongly against CSS-in-JS. If you didn't like CSS, then use SASS/LESS. Why mix styling with your code?

Well, turns out I was wrong. Styling is code and I have never felt more productive styling components than I do using CSS-in-JS. Next.js comes preloaded with styled-jsx, which is fine, but I prefer styled-components πŸ’…

If you want learn more, check out Building a UI Component Library with Styled Components.

Font Loading

Performant font loading is much, much more difficult than I thought. I'm immensely greatful to Zach Leatherman for his Comprehensive Guide on Web Fonts. Every option has their pros/cons, but the approach I've taken for this site is to use @font-face in combination with the font-display attribute.

This approach allows me prevent FOUT (Flash of Unstyled Text) but still has FOIT (Flash of Invisible Text). Yes, FOIT is undeserable, but thanks to Next.js my Lighthouse performance score is still almost 100.

@font-face {
    font-family: 'Fira Sans';
    src: url('/static/fonts/FiraSans-Bold.ttf');
    font-weight: bold;
    font-display: auto;
    font-style: normal;

I've used some of the other font-loading approaches in other scenarios. Becomming informed about all the different options and their tradeoffs has been a worthy investment.

Dynamic Assets and Testing

It's likely that at some point in scaling your Next.js app, you'll want to use an external package that doesn't work well server-side rendered. For me, this package was react-select.

When SSR this component, it simply did not work in Safari. Until that bug is fixed, I needed a workaround. Thanks to Next's Dynamic Imports, it's easy to import a component and disable SSR.

import dynamic from 'next/dynamic';

const ReactSelectNoSSR = dynamic(() => import('../components/select'), {
    ssr: false

export default () => (
        <Header />
        <ReactSelectNoSSR />
        <Footer />

While this works, we can go a step further and provide a loading placeholder to make the user experience better.

const ReactSelectNoSSR = dynamic(() => import('../components/select'), {
    loading: () => <Input />,
    ssr: false

Much better πŸŽ‰ Now, how do we test it?

My preferred testing library is Jest. The dynamic import support offered by Next.js does not expose a way to preload the dynamically imported components in Jest’s environment. However, thanks to jest-next-dynamic, we can render the full component tree instead of the loading placeholder. Perfect!

You'll need to add babel-plugin-dynamic-import-node to your .babelrc like so.

  "plugins": ["babel-plugin-dynamic-import-node"]

Then, you can use preloadAll() to render the component instead of the loading placeholder.

import preloadAll from 'jest-next-dynamic';
import ReactSelect from './select';

beforeAll(async () => {
    await preloadAll();


My portfolio has transformed quite a bit since I started it in 2014. Originally, it was just static HTML & CSS. Then, I switched over to Hugo so I could write my posts in Markdown. This worked well for awhile, but I wanted full control over my layouts and felt much more comfortable with JavaScript. That's what led me to Next.js and MDX.

With MDX, I can use JSX components inside my Markdown documents with ease. This way, if I want to do something custom, it's as simple as importing a React component. I've also started writing my presentations with MDX and code-surfer πŸ„

For maximum performance, you can use the prefetch attribute on Next's <Link> component. This will give the perception the page loads instantly. As of Next.js 8, prefetch uses <link rel="preload"> instead of a <script> tag. It also only starts prefetching after onload to allow the browser to manage resources.

<Link prefetch href="/">

Bonus: Next won't prefetch if the user is on 2G. Pretty neat!


Next.js supports IE11 and all modern browsers out of the box using @babel/preset-env. However, it's possible that your own code or external NPM dependencies might use features not supported in your target browsers. In this case, you will need to add polyfills. This requires changes in a couple of places.

First, you will need to create/override your next.config.js file to customize Webpack.

module.exports = {
    webpack: function(cfg) {
        const originalEntry = cfg.entry;
        cfg.entry = async () => {
            const entries = await originalEntry();

            if (entries['main.js'] && !entries['main.js'].includes('./client/polyfills.js')) {

            return entries;

        return cfg;

Then, create /client/polyfills.js and include whichever polyfills you need.

import '@babel/polyfill';


Now (also created by ZEIT) is hands down the easiest way to deploy applications I've ever used. Yes, I'm definitely drinking the ZEIT kool-aid. It's incredibly easy to get started and their GitHub integration will automatically deploy your app on pull requests and leave a link for you to review the changes. If everything looks good, it will deploy to prod when the PR is merged. Simple as that.

The icing on the cake is how well the ZEIT ecosystem works together. Their domain service allows you to buy domains from the command line. I've never been able to go from an idea to a live, deployed application hosted on a domain so fast.

Bonus: I was even able to setup email forwarding for my domain and create using improvmx πŸŽ‰

Where to Get Help

The Next.js and larger ZEIT community support is fantastic. They're very responsive to emails, issues, or any other form of contact. My preferred method would be on Spectrum. Thanks to Spectrum, all questions and answers are indexed, searchable, and easy to find. Their tutorials and docs are also fantastic and well-written.

Additional Resources

Discuss on Twitter β€’ Edit on GitHub

Want More? Be Notified When I Post New Articles πŸš€