ReactHustle

How to Create an Efficient Code Syntax Highlighter Component in React using PrismJS

Jasser Mark Arioste

Jasser Mark Arioste

How to Create an Efficient Code Syntax Highlighter Component in React using PrismJS

Hello, hustlers! In this tutorial, you'll learn how to create an efficient code syntax highlighter component in React using PrismJS. This component won't slow down your page even when you have a lot of code in it.

Introduction - The Problem #

Recently, I was checking my core web vitals in Google LightHouse for Desktop. I noticed that some of the pages with high code content had high Total Blocking Time (TBT). The TBT was quite high (500ms) for desktop screens is pretty weird. 

I debugged it and found out that the problem was using the Prism.highlightAll() function. This is the function recommended by many guides on the internet. However, this is actually not good, and costly since it finds all the <pre/> and <code/> elements on the page. It tries to highlight all elements at once.

Another problem I faced in Lighthouse was the "Avoid an Excessive DOM Size" warning. Some of my pages were reaching a whopping 1900+ elements. This happens because PrismJS transforms each token (function names, variables, reserved keywords, and others.) in the code into a <span/> element. This is also a big problem that slowed down the load times of this blog.

Solution #

To solve these two problems, we only need one solution.

Use the IntersectionObserver API and Prism.highlightAllUnder() function to highlight the <pre/> and <code/> tags only when they are visible in the viewport.

All right, let's start coding!

Step 1 - Installing the dependencies #

First, install prism.js by running the command:

yarn add prismjs classnames # we also install classnames to easily manage the classes

yarn add -D @types/prismjs #be sure to add typings if you're using typescript
123

Step 2 - Creating the <SyntaxHighlighter/> Component. #

Let's create a simple <SyntaxHighlighter/> component. First, create the file components/SyntaxHighlighter.tsx.

// components/SyntaxHighlighter.tsx
// import prism and the languages you need.
import Prism from "prismjs";
import cn from "classnames";

import "prismjs/components/prism-jsx";
import "prismjs/components/prism-tsx";

// include line numbers and line highlights plugin,
import "prismjs/plugins/line-numbers/prism-line-numbers";
import "prismjs/plugins/line-highlight/prism-line-highlight";

// include css for line numbers and highlights
import "prismjs/plugins/line-numbers/prism-line-numbers.css";
import "prismjs/plugins/line-highlight/prism-line-highlight.css";

//import theme
import "prismjs/themes/prism.min.css";
import { useEffect, useRef } from "react";

type Props = {
  code: string;
  language: string;
  showlineNumbers?: boolean;
  lineHighlights?: number[];
};

const SyntaxHighlighter = ({
  showlineNumbers = true,
  language,
  code,
  lineHighlights,
}: Props) => {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    if (ref.current) {
      // highlight this specific component only.
      // ! Do not use Prism.highlightAll().
      Prism.highlightAllUnder(ref.current);
    }
  }, []);

  return (
    <div ref={ref}>
      <pre
        className={cn({
          "line-numbers": showlineNumbers,
          [`language-${language}`]: true,
        })}
        data-line={lineHighlights?.join(",")}
      >
        <code>{code.trim()}</code>
      </pre>
    </div>
  );
};

export default SyntaxHighlighter;

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758

Explanation:

We define some props that we probably need such as code, language, etc. Next, we use classnames to set/toggle classes for the language and line numbers. Then, we use the data-line property for the line highlights. These can all be found in the prismjs documentation.

We then use Prism.highlightAllUnder(element:Element) function to specifically highlight the contents of this component.

Note that this component is not yet optimized, if we render a lot of the <SyntaxHighlighter/> component on the page, it will still slow down the page.

Usage:

import SyntaxHighlighter from "../components/SyntaxHighlighter";

const Component = () => {
  const code = `
<div className={styles.container}>
  <main>
    Hello,world
  </main>
</div>`;
  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <SyntaxHighlighter code={code} language="jsx" lineHighlights={[2, 4]} />
      </main>
    </div>
  );
};

export default Component;
12345678910111213141516171819

And here's the output:

React PrismJS Syntax Highlighter output after Step 2

Step 3 - Optimizing using IntersectionObserver #

Let's modify the SyntaxHighlighter component so that it only highlights and tokenizes the code if the component is visible on the viewport. I've added comments to make it more understandable so be sure to read them.

// components/SyntaxHighlighter.tsx
// import prism and the languages you need.
import Prism from "prismjs";
import cn from "classnames";

import "prismjs/components/prism-jsx";
import "prismjs/components/prism-tsx";

// include line numbers and line highlights plugin,
import "prismjs/plugins/line-numbers/prism-line-numbers";
import "prismjs/plugins/line-highlight/prism-line-highlight";

// include css for line numbers and highlights
import "prismjs/plugins/line-numbers/prism-line-numbers.css";
import "prismjs/plugins/line-highlight/prism-line-highlight.css";

//import theme
import "prismjs/themes/prism.min.css";
import { useEffect, useRef, useState } from "react";

type Props = {
  code: string;
  language: string;
  showlineNumbers?: boolean;
  lineHighlights?: number[];
};

const SyntaxHighlighter = ({
  showlineNumbers = true,
  language,
  code,
  lineHighlights,
}: Props) => {
  // 1. Add a state to track if the component has already been highlighted
  const [hihlighted, setHighlighted] = useState(typeof window === "undefined");
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    if (ref.current) {
      // 2. create an IntersectionObserver to observe the ref to the div wrapper element.
      const observer = new IntersectionObserver(
        (entries) => {
          entries.forEach((entry) => {
            // 4. Check if it's showing and not yet higlighted
            if (entry.isIntersecting && !hihlighted) {
              setHighlighted(true);
              setTimeout(() => {
                Prism.highlightAllUnder(entry.target);
              }, 50);
            }
          });
        },
        // 5. Add root margin so that we can highlight the code in advance before it shows to the screen.
        {
          rootMargin: "100%",
        }
      );
      // 3. Wire up ref and observer
      observer.observe(ref.current);
    }
  }, []);

  return (
    <div ref={ref}>
      <pre
        className={cn({
          "line-numbers": showlineNumbers,
          [`language-${language} `]: true,
        })}
        data-line={lineHighlights?.join(",")}
      >
        <code>{code.trim()}</code>
      </pre>
    </div>
  );
};
export default SyntaxHighlighter;
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576

Next, render more than one <SyntaxHighlighter/> component such that some of them are off the screen.

import SyntaxHighlighter from "../components/SyntaxHighlighter";

const Component = () => {
  const code = `
<div className={styles.container}>
  <main>
    Hello,world
  </main>
</div>`;
  return (
    <div className={styles.container}>
      <main className={styles.main}>
        {Array.from([new Array(20)].keys()).map((i) => {
          return (
            <div key={i}>
              <p style={{ height: 400 }}>Lorem ipsum</p>
              <SyntaxHighlighter
                code={code}
                language="jsx"
                lineHighlights={[2, 4]}
              />
            </div>
          );
        })}
      </main>
    </div>
  );
};

export default Component;
123456789101112131415161718192021222324252627282930

In the above code, we're trying to render 20 <SyntaxHighlighter/> components but only 2 are displayed as shown below:

Optimizing PrismJS Syntax highlighter using IntersectionObserver

If we check the logs, we can see that the highlighting only ran twice! Once you scroll to the bottom you'll see that it ran 20 times.

Optimizing PrismJS Syntax highlighter using IntersectionObserver - B

This also fixes the excessive dom size problem since the code is not split into span elements before highlighting thereby keeping the number of elements small.

That's basically it!

Code and Demo #

The full code is available on GitHub: React PrismJS Tutorial. Note that this uses NextJS as the react framework of choice.

The demo is available on Stackblitz: React PrismJS Tutorial. In the demo, I rendered around 200 <SyntaxHighlighter/> components, try to scroll to the bottom quickly and see that some of the codes are not highlighted before you scroll to them.

Conclusion #

We learned how to create an efficient syntax highlighter component that doesn't slow down the page load speed by using the IntersectionObserver API and Prism.highlightAllUnder() function.

If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on Twitter.

Credits: Image by gsibergerin from Pixabay

Share this post!

Related Posts