You will need to open the folder perf from the repo in your project.

Let's open the perf project in our repo and run npm install.

We're going to build a markdown previewer! On some laptops this can be pretty slow to parse and re-render to the DOM. On my laptop it's actually fast enough to get through it so we're going to introduce some artificial jank. Your computer may not need it.

I left a long markdown file for you to use as a sample in markdownContent.js. I asked Claude to make some jokes for us. I'd call it a middling success.

Make a new file called MarkdownPreview.jsx. Put this in there

const JANK_DELAY = 100;

export default function MarkdownPreview({ render, options }) {
  const expensiveRender = () => {
    const start = performance.now();
    while (performance.now() - start < JANK_DELAY) {}
    return null;
  };
  return (
    <div>
      <h1>Last Render: {Date.now()}</h1>
      <div
        className="markdown-preview"
        dangerouslySetInnerHTML={{ __html: render(options.text) }}
        style={{ color: options.theme }}
      ></div>
      {expensiveRender()}
    </div>
  );
}
  • This is the artificial jank. It ties up the main thread so it'll run slower. Feel free to modify JANK_DELAY. Right now I have it delaying 100ms so you can see the jank more pronounced.
  • I'm also showing the time so can see how often the component runs.
  • dangerouslySetInnerHTML is fun. It just means you need to really trust what you're putting in there. If you just put raw user generated content in there, they could drop a script tag in there and do a good ol' XSS attack. In this case the user can only XSS themself so that's fine enough.

Okay, let's make our App.jsx

import { useEffect } from "react";
import { marked } from "marked";
import { useState } from "react";

import MarkdownPreview from "./MarkdownPreview";
import markdownContent from "./markdownContent";

export default function App() {
  const [text, setText] = useState(markdownContent);
  const [time, setTime] = useState(Date.now());
  const [theme, setTheme] = useState("green");

  useEffect(() => {
    const interval = setInterval(() => {
      setTime(Date.now());
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  const options = { text, theme };
  const render = (text) => marked.parse(text);

  return (
    <div className="app">
      <h1>Performance with React</h1>
      <h2>Current Time: {time}</h2>
      <label htmlFor={"theme"}>
        Choose a theme:
        <select value={theme} onChange={(e) => setTheme(e.target.value)}>
          <option value="green">Green</option>
          <option value="blue">Blue</option>
          <option value="red">Red</option>
          <option value="yellow">Yellow</option>
        </select>
      </label>
      <div className="markdown">
        <textarea
          className="markdown-editor"
          value={text}
          onChange={(e) => setText(e.target.value)}
        ></textarea>
        <MarkdownPreview options={options} render={render} />
      </div>
    </div>
  );
}

Alright, go play with it now (you may need to mess with the JANK_DELAY as well as the interval of how often the interval runs). The scroll is probably either janky or it has a hard time re-rendering. Typing in it should hard as well. Also notice that the current render. Re-rendering the theme is tough too.

So how can we fix at least the scroll portion, as well make the other two a little less painful (as they'll only re-rendering once as opposed to continually.)