Skip to content

processStylexRules is O(rules × consts) when defineConsts is used #1960

Description

@maxmurdoch

Version: @stylexjs/babel-plugin 0.19.0 and 0.19.1

processStylexRules substitutes defineConsts values with a nested loop. For every non-const rule and every direction, it iterates over all consts and calls replaceAll twice:

for (const [varRef, constValue] of constsMap.entries()) {
  original = original.replaceAll(varRef, replacement);
  // ...and replaceAll(`${constName}:`, `${targetName}:`) for var() consts
}

So the cost grows with rules × consts. With a few hundred consts (for example, a typed token module of var(--…) constants) and a few thousand rules, the CSS build goes from milliseconds to hundreds of milliseconds. In dev, @stylexjs/unplugin calls processStylexRules on every CSS request, and the Vite plugin triggers one whenever a transformed module changes the rule set. That makes dev servers and browser test runs noticeably slower.

Repro (public API only):

const { processStylexRules } = require("@stylexjs/babel-plugin");
const CONSTS = 200, RULES = 6000;
const rules = [];
for (let i = 0; i < CONSTS; i++)
  rules.push([`c${i}`, { constKey: `c${i}`, constVal: `var(--token-${i})`, ltr: "", rtl: null }, 0]);
for (let i = 0; i < RULES; i++)
  rules.push([`x${i}`, { ltr: `.x${i}{color:var(--c${i % CONSTS})}`, rtl: null }, 3000]);
console.time("processStylexRules");
processStylexRules(rules, { useLayers: true });
console.timeEnd("processStylexRules");

This takes 169ms with 0.19.0, and 16ms with the change below. On a real app (6,242 rules, about 200 consts) it takes 313ms before and 12ms after.

Proposed fix: resolve consts once (as today), precompute each const's custom-property rename target, then substitute in one pass per rule string:

const constTargetNames = new Map(); // "--hash" -> "--target" for var() consts
// ...
const constRefRegex = /var\(--[A-Za-z0-9_-]+\)|--[A-Za-z0-9_-]+:/g;
styleObj[dir] = original.replace(constRefRegex, (match) => {
  if (match.startsWith('var(')) {
    const v = constsMap.get(match);
    return v == null ? match : String(v);
  }
  const target = constTargetNames.get(match.slice(0, -1));
  return target == null ? match : `${target}:`;
});

The output is byte-identical to the current implementation on the two real rule sets I compared (6,242 and 906 rules, with and without useLayers). I can open a PR.

Activity

  1. henryqdineen commented on Oct 19, 2026

    @henryqdineen
    Collaborator

    We are running into similar perf issues and have open PR to hopefully fix in #1700.

  2. self-assigned this
    on Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions