next.jsserver-side-renderingmismatchprismjs

Nextjs: server rendered code blocks highlighted by Prismjs mismatches and causes re-render due to leading whitespace on class attribute


I am trying to implement server-side rendering of syntax highlighted (tokenized) code blocks using Prismjs (note: I know how to do this using client-side rendering via useEffect and refs, and I got this working using prism-react-renderer. I am looking specifically for the solution for "bare" Prismjs & SSR).

// codeExamples = array of some code strings

const Code = ({ code, language }) => {
  const html = Prism.highlight(code, Prism.languages[language], language);
  return (
    <pre
      data-language={language}
      className={`language-${language}`}
    >
      <code
        dangerouslySetInnerHTML={{
          __html: html,
        }}
      />
    </pre>
  );
};

export default function Home() {
  return codeExamples.map((example, i) => (
    <Code
      language="javascript"
      code={example}
      key={i}
    ></Code>
  ));
}

It does work to some extent, but I've run into an issue: the code block gets briefly re-rendered, probably because of a leading whitespace on the class attribute:

enter image description here

This is causing (besides the poinless expensive re-render) a non-pleasant layout shift, temporarily fixed by adding hard-coded font-size in pixels to a <pre> element.

I've got some occasional warnings, either for server and client props mismatch (can't reproduce right now) or for Extra attributes from the server: class – but only when running next dev, not while running next build && next start.

Tested on the latest versions of Nextjs and Prism.


Solution

  • I've got an answer from a dive into Prismjs source code:

    if (parent && parent.nodeName.toLowerCase() === 'pre') {
        parent.className = parent.className.replace(lang, '').replace(/\s+/g, ' ') + ' language-' + language;
    }
    

    This is where the whitespace comes from. It is quite obvious, why it is here (as a padding for string concatenation).

    A temporary hacky fix is to add a leading whitespace to a className on <pre> element as well.

    <pre
       data-language={language}
       className={` language-${language}`}
    >
    

    But it is quite clear that this is not a good way to use Prismjs for SSR.