DEV Community

Ranieri Althoff
Ranieri Althoff

Posted on • Edited on

Speed up Next.js build with Typescript and Tailwind CSS

In the project I'm currently working with, we have a platform split between a single-page React app and a Next.js SSR website. The development server runs both in the same machine, using containers, but it has only one processor core and 2 GB of RAM.

The React SPA is big, and it takes a while to build. Next.js is notably very slow to build too, even if we only have a handful of pages. I'll document my quest to reduce the unbelievable build times, ranging from at least 5 minutes up to 22 (!) minutes when both are building in parallel.

Setting the baseline

I will not be running the tests on the dev server, but on my machine. I have a Ryzen 7 4800H with 16 GB of RAM, so the build will not be bottlenecked by hardware as heavily as the aforementioned server.

On this machine, the React SPA takes 84s to build, and Next.js takes 112s. We will reduce the time spent build CSS, and then on building Typescript.

CSS: @tailwindcss/jit

Update: You don't need to install a separate package anymore. Just add mode: 'jit' to your tailwind.config.js file and you are good to go! I will keep reading this section for historic purposes :)

The big issue with Tailwind is that it pulls a huge amount of CSS classes into the parser, in the form of a 3.6 MB CSS file - or 6.0 MB if you enable dark mode. This is heavy on PostCSS, uses a lot of memory (our server would frequently trigger OOM kills).

Enter @tailwindcss/jit, a drop-in replacement (though not with 100% feature parity yet) that collects the classes used by your files and only generates the requested classes.

Outdated PostCSS plugins

In this process, we had to get rid of two PostCSS plugins that were not updated to work with PostCSS 8, which is required by @tailwindcss/jit: precss and postcss-rtl, both of which are unmaintained and won't be updated.

We replaced the former with postcss-nesting and rewritten the CSS to not use the rest of the features from precss - in our case, this was an easy job.

The latter was replaced by tailwindcss-rtl, which is instead a Tailwind plugin. It works great and requires very little effort, you just need to search and replace some non-RTL classes with RTL equivalents - which I expect to be easily swappable once Tailwind supports CSS Logical Properties.

These are the changes required in webpack.config.js:

    module: {
        rules: [
            ...
            {
                test: /\.css$/,
                use: [
                    ...,
                    {
                        loader: 'postcss-loader',
                        options: {
-                           postcssOptions: { plugins: ['postcss-import', 'postcss-url', 'tailwindcss', 'precss', 'postcss-rtl'] },
+                           postcssOptions: { plugins: ['postcss-import', 'postcss-url', '@tailwindcss/jit'] },
                        },
                    },
                ],
            },
            ...
        ],
    },
    ...
Enter fullscreen mode Exit fullscreen mode

Don't forget to update your tailwind.config.js file, the instructions are in the @tailwindcss/jit README.

Results

This has the benefit of not only reducing the load on the parser, but also generating a much smaller CSS file by default, even without minification: a drop from 17.2 MB to 461 KiB before PurgeCSS for the SPA, and from 110 KiB to 1.34 KiB for the Next.js website.

With only that change so far, it reduces the build time to 66s for the SPA and to an amazing 26s for Next.js. I guess that the SPA didn't benefit as much because it uses much more classes - as evidenced by the significantly smaller CSS output.

Typescript: esbuild & esbuild-loader

esbuild is a JS and TS bundler that promises ultra-fast build times. We use webpack, and there is support to leverage esbuild with esbuild-loader.

Using esbuild-loader is really simple, we just need to swap ts-loader (or babel-loader, but I didn't use that) with esbuild-loader in your webpack.config.js:

    ...
    module: {
        rules: [
            ...
            {
                test: /\.tsx?$/,
                use: [
                    'cache-loader',
-                   'ts-loader',
+                   {
+                       loader: 'esbuild-loader',
+                       options: { loader: 'tsx' },
+                   },
                ],
            },
            ...
        ],
    },
    ...
Enter fullscreen mode Exit fullscreen mode

Furthermore, since we use the new JSX transform and esbuild does not support it yet, instead transforming JSX to calls to React.createElement which is not available, we need to provide a global React variable. This is super simple with webpack:

+const { ProvidePlugin } = require('webpack');

    ...
    plugins: [
        ...
+       new ProvidePlugin({
+           React: 'react',
+       }),
    ],
    ...
Enter fullscreen mode Exit fullscreen mode

The improvement, in real life, is not as huge as advertised in esbuild's website - I guess because it is not pure JS/TS - but still very significant: building the SPA now takes less than 20s. However, we didn't see as much improvement for Next.js, now taking about 22s. It doesn't hurt, so we will keep it 🤷

Conclusion

We brought down the build time from 84s down to 20s for a React SPA, and from a painful 112s down to 22s for a Next.js SSR website by just swapping the tools used to build. No features were harmed in the process, and we even ended up with less generated CSS.

Additionally, our pipelines used to take at least 5 minutes for a full build and deploy, and now only take around 3 minutes, caching and spinning the containers included, and it does not run out of RAM anymore. Mission accomplished.

On a separate note, we haven't suffered from this out-of-memory situation since we made the switch, and it seems to be related with excessive amounts of CSS.

Top comments (7)

Collapse
 
kwbtsts profile image
kwst

Hi! Thank you for helpful information.

I'm trying to apply esbuild on my nextjs project. I got this error. How can I fix it? Do you have any idea?

TypeError: this.getOptions is not a function
> Build error occurred
Error: > Build failed because of webpack errors
Enter fullscreen mode Exit fullscreen mode
Collapse
 
rsa profile image
Ranieri Althoff

It usually means that you are using an incompatible version of the plugin. Try installing a previous version of esbuild-loader or update your webpack instalation.

Collapse
 
sunaulosuhail profile image
Suhail Khan

Hello sir this was a great article, I am beginner in react, nextjs if possible can you please share final code please.

Collapse
 
rsa profile image
Ranieri Althoff

Hey! I couldn't share much more since this is a closed source application. However, this pattern is becoming more and more common now, soon you'll see open source applications using this toolkit

Collapse
 
sunaulosuhail profile image
Suhail Khan

Thanks for your reply, I actually tried following as you wrote in the article but no success, specially since I am not using custom next.js config file and had hard time updating it. Thanks a lot

Collapse
 
doctorderek profile image
Dr. Derek Austin 🥳

Great, thanks for sharing the tips about the JSX transform, loader, and setup configuration.

Collapse
 
sunaulosuhail profile image
Suhail Khan

Sir have to implemented those in your code base, if yes and if possible can you share final version of the code. I am just getting started with nextjs and don't have much idea customizing next config files.