May 4, 2023
3 min read▪︎

Reducing the size of MDX data in Next.js

How I reduced page sizes of my blogposts by almost a half.

The Problem

This site was made using Next.js. These blogposts are written using MDX, and they are rendered with next-mdx-remote. When building the site, Next.js renders the blogposts as HTML using server-side rendering. Upon visiting a page, the static HTML is initially served to the client, after which the page is hydrated, allowing for interactive React components otherwise not possible with static HTML.

This build-time rendering has advantages, such as fast initial load time (before hydration), improved SEO, and making the content accessible even if JavaScript is disabled. The disadvantage of this approach is that the content of the blogpost is effectively included twice in the page: once as static HTML in the page source, and the second time as compiled MDX (JSX), embedded in the __NEXT_DATA__ object at the bottom of the page, which is used when hydrating the page.

For example, the __NEXT_DATA__ object in this blogpost is 1.16 MB, the majority of it being the compiled MDX source. Going through the compiled MDX source, I noticed that some parts of it contained a lot of whitespace, which seemed unnecessary (see image below). Reducing the size of the compiled MDX source should result in a smaller __NEXT_DATA__ object, and thus a smaller page size, making it faster to load.

Compiled MDX source embedded in the page.
Compiled MDX source embedded in the page.

The solution

The solution was to minify the compiled MDX source in getStaticProps, before passing it to the page. Here’s the getStaticProps function I was using with my blog posts:

export const getStaticProps = async ({ params }) => {
  const postFilePath = path.join(NOTES_PATH, `${params.slug}.mdx`);
  const source = fs.readFileSync(postFilePath);

  const { content, data } = matter(source);
  const mdxSource = await compileMDX(content, data);

  return {
    props: {
      source: mdxSource,
    },
  };
};

I used terser to minify the compiled MDX source. The updated code looks like this:

import { minify } from "terser";

// ...

export const getStaticProps = async ({ params }) => {
  const postFilePath = path.join(NOTES_PATH, `${params.slug}.mdx`);
  const source = fs.readFileSync(postFilePath);

  const { content, data } = matter(source);
  const mdxSource = await compileMDX(content, data);

  mdxSource.compiledSource = (
    await minify(mdxSource.compiledSource, {
      parse: {
        bare_returns: true,
      },
      toplevel: true,
    })
  ).code;

  return {
    props: {
      source: mdxSource,
    },
  };
};

With this, the size of the __NEXT_DATA__ object of the same blogpost is now 515 kB, which is less than half of the original size. Looking at the compiled MDX source, the whitespace is now effectively gone (see image below).

Compiled MDX source after minification.
Compiled MDX source after minification.

After building the site with, I checked the size of the static HTML file of the blogpost. The results are shown in the table below.

Original MDXMinified MDXSize reduction
__NEXT_DATA__ object1.16 MB515 kB55.6%
Static HTML809.41 kB460.62 kB43.1%

Wrapping Up

In conclusion, minifying the compiled MDX source is a simple yet effective way to cut down on page size, and make your site faster to load.