I'm building this website with Next.js and charmed by the delightful developer experience we get when it's combined with Vercel.
In addition to eliminating the friction, Vercel also provides analytics and Lighthouse integration to monitor how my website is performing when it comes to vital metrics for the user experience.
And that's precisely what I did right after deploying a bearable version. But the results were not satisfying at all.
My minimal home page with three lines of text and a handful of links made Lighthouse complain sorely about the performance.
The report is clear, it tells that I'm basically making your computer load and parse a bunch of irrelevant JavaScript.
I thought that's an ethos to have if you are building a publishing thingy nowadays, or am I in the wrong Medium? ๐น๐น๐น
Finding out what I am excessively loading
The report shows which chunk is not utilized. Unfortunately, the code is minified.
I have two choices:
-
Be a responsible, clean coder and inspect the source maps by using @next/bundle-analyzer to find out what's wrong.
-
Remember that's a fun project, and double the amount of fun with puzzles by guesstimating what it could be.
Proceeding with the latter, I know one thing which doesn't get minified: error messages!
And searching for only one of 'allowedTypes' and 'disallowedTypes' should be defined
in the node_modules
is a good next step for approaching to the crux.
Then I realize it's seemingly related to markdown. Now that the scope is narrowed down, it's time to discover what I'm doing wrong.
How does this website render markdown
I tend to rely on markdown for rendering any kind of content, and using react-markdown to render raw content to HTML. It has a nice API that allows you to render specific elements by using custom renderers.
I copy/pasted the snippet in docs to render code blocks with react-syntax-highlighter.
It looked like this before the refactor:
// Markdown.jsx import ReactMarkdown from 'react-markdown' import { Prism } from 'react-syntax-highlighter' const Code = (props) => <Prism language={props.language}>{props.value}</Prism> const Markdown = ({ children }) => ( <ReactMarkdown renderers={{ code: Code, }} > {children} </ReactMarkdown> ) export default Markdown
That might be a heavy library to load if I'm not planning to render any code blocks since it contains all the parsing logic with themes and such.
And as you can guess, the homepage does not contain any code snippets, which is why Lighthouse is unhappy: the eager load.
Dynamically loading modules with Next.js
Although React
allows lazy loading with the suspense API, it's not available for Next.js
since the ReactDOMServer
support has not implemented yet.
Luckily, our thoughtful friend Next.js
offers an alternative API for dynamic imports. It's called next/dynamic and pretty straightforward for such a use-case like this.
First, I changed my usage of react-syntax-higlighter
to load and register parsers only for the languages I use.
// MarkdownCodeBlock.jsx import { PrismLight } from 'react-syntax-highlighter' import jsx from 'react-syntax-highlighter/dist/cjs/languages/prism/jsx' import tsx from 'react-syntax-highlighter/dist/cjs/languages/prism/tsx' PrismLight.registerLanguage('jsx', jsx) PrismLight.registerLanguage('tsx', tsx) const MarkdownCodeBlock = ({ language, value }) => ( <PrismLight language={language}>{value}</PrismLight> ) export default MarkdownCodeBlock
I also changed the strategy of using syntax highlighter to be on demand, so the other pages using markdown (such as book notes) won't load react-markdown
and friends.
// Markdown.jsx import ReactMarkdown from 'react-markdown' import dynamic from 'next/dynamic' const MarkdownCodeBlock = dynamic(() => import('./MarkdownCodeBlock')) const Markdown = ({ children }) => ( <ReactMarkdown renderers={{ code: MarkdownCodeBlock, }} > {children} </ReactMarkdown> ) export default Markdown
...and then, did they live happily after? ๐ค๐จโ๐ป
In the end, the performance score of the home page increased dramatically.
This approach doesn't eradicate the CPU tax of loading and executing the JS on the pages that I need to render a code editor, such as this post.
Nonetheless, it divides the amount of work while browsing and progressively loads the required code, which is more helpful than not doing it. ๐คจ