Compute once, reuse

Lazy-init state, cache pure results, and build each RegExp once.

Vercelvercel.com/blog
Reference: How we made the Vercel Dashboard twice as fast
Vercel BlogRead article ↗

From vercel-react-best-practices: rerender-lazy-state-init and 2 more.

Lazy state initialization

Type in both boxes, then raise the item count.

Eager argumentBad
buildSearchIndex calls
1
Last render
0 ms

0 ms16 ms frame32 ms

    useState(buildSearchIndex(items))discarded 0
    Lazy initializerGood
    buildSearchIndex calls
    1
    Last render
    0 ms

    0 ms16 ms frame32 ms

      useState(() => buildSearchIndex(items))
      function FilteredList({ items }) {
        const [index] = useState(() => buildSearchIndex(items))
        const [query, setQuery] = useState('')
        return <SearchResults index={index} query={query} />
      }

      Cache repeated calls in a Map

      Press Play, then drag Unique names.

      slugify() in the mapBad
      This render
      0
      Total calls
      0
      Renders
      0
      slugify(project.name)
      cachedSlugify()Good
      This render
      0
      Total calls
      0
      Cache size
      0
      slugifyCache.has(text)
      const slugifyCache = new Map()
      
      export function cachedSlugify(text) {
        if (slugifyCache.has(text)) return slugifyCache.get(text)
        const result = slugify(text)
        slugifyCache.set(text, result)
        return result
      }

      Memoize patterns per query

      Type c++ or (draft, then turn on re-renders.

      new RegExp in renderBad
      new RegExp calls
      0
      Renders
      0
        new RegExp(`(${query})`, 'gi')
        useMemo on [query]Good
        new RegExp calls
        0
        Renders
        0
          escapeRegex(query)
          const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
          
          function Highlighter({ text, query }) {
            const regex = useMemo(
              () => new RegExp(`(${escapeRegex(query)})`, 'gi'),
              [query]
            )
            const parts = text.split(regex)
            return parts.map((part, i) => (i % 2 ? <mark key={i}>{part}</mark> : part))
          }

          Why it matters, pros and cons

          Why it matters

          Arguments run before the call, so useState(build()) rebuilds on every render.

          Pros

          • Keystroke cost stops scaling with data
          • A module Map works outside components too
          • Escaping the query also fixes crashes

          Cons

          • Cheap initial values need no function form
          • A lazy initializer never rebuilds when items changes
          • Module Maps are unbounded and shared on the server