perf(react_compiler): keep small hot-path collections inline (#24514)

## What

Two kinds of small, hot collections in the react-compiler pass get
heap-allocated on every use:

- The HIR operand/lvalue visitors (`each_instruction_value_operand`,
`each_terminal_operand`, and friends) build a fresh `Vec<Place>` for
every instruction they're called on, and nearly every pass calls them in
a loop. The lists almost always hold one to four entries.
- The aliasing fixpoint allocates a successor list per block visit
(`terminal_successors`) and an args/captures list per interned effect
(`EffectKey`).

## Change

- The visitors now return `PlaceList = SmallVec<[Place; 4]>`; element
order and contents are unchanged.
- `terminal_successors` returns a `SmallVec<[BlockId; 2]>`, and
`EffectKey::Apply` args / `CreateFunction` captures are inline small
vectors, so building a typical key no longer allocates.
- Adds `smallvec` to the crate (already a workspace dependency).

Numbers from the stock release profile (fat LTO), linting cal.com with
`react/react-compiler` as the only enabled rule, `--threads=1`:

|              |         before |          after |               delta |
| ------------ | -------------: | -------------: | ------------------: |
| instructions | 18,085,644,607 | 17,200,052,223 |               −4.9% |
| wall time    |        2.787 s |        2.754 s | −1.2% |

## Verification

- Lint output is byte-identical to main on cal.com and excalidraw, with
`reportAllBailouts` both on and off (full stdout/stderr and exit codes,
single-threaded).
- Transform output is identical on all 1,438 tsx/jsx files across the
two corpora, with no exit-code changes.
- `cargo test -p oxc_react_compiler` (including the fixture snapshot
suite), clippy, and rustfmt are all clean.

This PR was assisted by Claude Code.
This commit is contained in:
Marius Schulz authored and GitHub committed 2026-07-15 00:14:01 +08:00
1 parent e6f7174029
commit b6850625af
6 files changed
+52 -40

No files matched your search

Generated
+1
View File
@@ -2447,6 +2447,7 @@ dependencies = [
"oxc_str",
"oxc_syntax",
"rustc-hash",
"smallvec",
]
[[package]]
+3 -2
View File
@@ -6,7 +6,7 @@
# This crate owns the oxc <-> react_compiler_ast conversion layer, plus the React
# Compiler *core* itself, vendored in-tree under `src/react_compiler*` (one module
# per upstream crate, from the MIT fork at oxc-project/forked-react-compiler). The
# core is frontend-agnostic (serde/indexmap only, never oxc); the AST/scope
# core is frontend-agnostic (small utility crates only, never oxc); the AST/scope
# conversion on our side is written against the live workspace oxc AST.
[package]
@@ -40,10 +40,11 @@ oxc_str = { workspace = true }
oxc_syntax = { workspace = true, features = ["to_js_string"] }
# External crates the vendored React Compiler modules (`src/react_compiler*`) depend
# on. The compiler core is frontend-agnostic — serde/indexmap/hashing only, no oxc.
# on. The compiler core is frontend-agnostic — small utility crates only, no oxc.
indexmap = { workspace = true }
rustc-hash = { workspace = true }
cow-utils = { workspace = true }
smallvec = { workspace = true }
[dev-dependencies]
# The example and integration tests parse source and print the compiled program; the
@@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
use rustc_hash::FxHashMap;
use smallvec::SmallVec;
use oxc_index::IndexSlice;
@@ -19,10 +20,14 @@ use crate::react_compiler_hir::{
// Iterator functions (return Vec instead of generators)
// =============================================================================
/// Operand/lvalue lists are almost always 1-4 entries; keep them inline instead
/// of heap-allocating a `Vec` for every instruction visited.
pub type PlaceList = SmallVec<[Place; 4]>;
/// Yields `instr.lvalue` plus the value's lvalues.
/// Equivalent to TS `eachInstructionLValue`.
pub fn each_instruction_lvalue(instr: &Instruction) -> Vec<Place> {
let mut result = Vec::new();
pub fn each_instruction_lvalue(instr: &Instruction) -> PlaceList {
let mut result = PlaceList::new();
result.push(instr.lvalue.clone());
result.extend(each_instruction_value_lvalue(&instr.value));
result
@@ -30,8 +35,8 @@ pub fn each_instruction_lvalue(instr: &Instruction) -> Vec<Place> {
/// Yields lvalues from DeclareLocal/StoreLocal/DeclareContext/StoreContext/Destructure/PostfixUpdate/PrefixUpdate.
/// Equivalent to TS `eachInstructionValueLValue`.
pub fn each_instruction_value_lvalue(value: &InstructionValue) -> Vec<Place> {
let mut result = Vec::new();
pub fn each_instruction_value_lvalue(value: &InstructionValue) -> PlaceList {
let mut result = PlaceList::new();
match value {
InstructionValue::DeclareContext { lvalue, .. }
| InstructionValue::StoreContext { lvalue, .. }
@@ -88,13 +93,13 @@ pub fn each_instruction_value_lvalue(value: &InstructionValue) -> Vec<Place> {
/// Delegates to each_instruction_value_operand.
/// Equivalent to TS `eachInstructionOperand`.
pub fn each_instruction_operand(instr: &Instruction, env: &Environment) -> Vec<Place> {
pub fn each_instruction_operand(instr: &Instruction, env: &Environment) -> PlaceList {
each_instruction_value_operand(&instr.value, env)
}
/// Yields operand places from an InstructionValue.
/// Equivalent to TS `eachInstructionValueOperand`.
pub fn each_instruction_value_operand(value: &InstructionValue, env: &Environment) -> Vec<Place> {
pub fn each_instruction_value_operand(value: &InstructionValue, env: &Environment) -> PlaceList {
each_instruction_value_operand_with_functions(value, &env.functions)
}
@@ -103,8 +108,8 @@ pub fn each_instruction_value_operand(value: &InstructionValue, env: &Environmen
pub fn each_instruction_value_operand_with_functions(
value: &InstructionValue,
functions: &IndexSlice<FunctionId, [HirFunction]>,
) -> Vec<Place> {
let mut result = Vec::new();
) -> PlaceList {
let mut result = PlaceList::new();
match value {
InstructionValue::NewExpression { callee, args, .. }
| InstructionValue::CallExpression { callee, args, .. } => {
@@ -282,8 +287,8 @@ pub fn each_instruction_value_operand_with_functions(
/// Yields each arg's place.
/// Equivalent to TS `eachCallArgument`.
pub fn each_call_argument(args: &[PlaceOrSpread]) -> Vec<Place> {
let mut result = Vec::new();
pub fn each_call_argument(args: &[PlaceOrSpread]) -> PlaceList {
let mut result = PlaceList::new();
for arg in args {
match arg {
PlaceOrSpread::Place(place) => {
@@ -299,8 +304,8 @@ pub fn each_call_argument(args: &[PlaceOrSpread]) -> Vec<Place> {
/// Yields places from array/object patterns.
/// Equivalent to TS `eachPatternOperand`.
pub fn each_pattern_operand(pattern: &Pattern) -> Vec<Place> {
let mut result = Vec::new();
pub fn each_pattern_operand(pattern: &Pattern) -> PlaceList {
let mut result = PlaceList::new();
match pattern {
Pattern::Array(arr) => {
for item in &arr.items {
@@ -421,8 +426,8 @@ pub fn each_terminal_successor(terminal: &Terminal) -> Vec<BlockId> {
/// Yields places used by terminal.
/// Equivalent to TS `eachTerminalOperand`.
pub fn each_terminal_operand(terminal: &Terminal) -> Vec<Place> {
let mut result = Vec::new();
pub fn each_terminal_operand(terminal: &Terminal) -> PlaceList {
let mut result = PlaceList::new();
match terminal {
Terminal::If { test, .. } => {
result.push(test.clone());
@@ -63,6 +63,8 @@ use crate::react_compiler_hir::type_config::ValueKind;
use crate::react_compiler_hir::type_config::ValueReason;
use crate::react_compiler_hir::visitors;
use oxc_span::Span;
use smallvec::SmallVec;
use smallvec::smallvec;
use std::cell::Cell;
use std::rc::Rc;
@@ -734,7 +736,7 @@ enum EffectKey {
receiver: IdentifierId,
function: IdentifierId,
mutates_function: bool,
args: Vec<ArgKey>,
args: SmallVec<[ArgKey; 4]>,
into: IdentifierId,
},
CreateFrom {
@@ -801,7 +803,7 @@ enum EffectKey {
CreateFunction {
into: IdentifierId,
function_id: FunctionId,
captures: Vec<IdentifierId>,
captures: SmallVec<[IdentifierId; 4]>,
},
/// Synthetic keys used by the `Assign` handler for kind-preserving copies;
/// they share the ValueId cache but never collide with real-effect keys.
@@ -826,7 +828,7 @@ enum ArgKey {
fn effect_key(effect: &AliasingEffect) -> EffectKey {
match effect {
AliasingEffect::Apply { receiver, function, mutates_function, args, into, .. } => {
let mut key_args: Vec<ArgKey> = args
let mut key_args: SmallVec<[ArgKey; 4]> = args
.iter()
.map(|a| match a {
PlaceOrSpreadOrHole::Hole => ArgKey::Hole,
@@ -3275,29 +3277,31 @@ fn create_temp_place(env: &mut Environment, span: Option<Span>) -> Place {
/// successors but NOT pseudo-successors (fallthroughs). Fallthroughs for
/// Logical/Ternary/Optional and Try/Scope/PrunedScope are reached naturally
/// via the block iteration order (blocks are stored in topological order).
fn terminal_successors(terminal: &Terminal) -> Vec<BlockId> {
fn terminal_successors(terminal: &Terminal) -> SmallVec<[BlockId; 2]> {
match terminal {
Terminal::Goto { block, .. } => vec![*block],
Terminal::If { consequent, alternate, .. } => vec![*consequent, *alternate],
Terminal::Branch { consequent, alternate, .. } => vec![*consequent, *alternate],
Terminal::Goto { block, .. } => smallvec![*block],
Terminal::If { consequent, alternate, .. } => smallvec![*consequent, *alternate],
Terminal::Branch { consequent, alternate, .. } => smallvec![*consequent, *alternate],
Terminal::Switch { cases, .. } => cases.iter().map(|c| c.block).collect(),
Terminal::For { init, .. } => vec![*init],
Terminal::ForOf { init, .. } | Terminal::ForIn { init, .. } => vec![*init],
Terminal::DoWhile { loop_block, .. } => vec![*loop_block],
Terminal::While { test, .. } => vec![*test],
Terminal::Return { .. } | Terminal::Throw { .. } | Terminal::Unreachable { .. } => vec![],
Terminal::Try { block, .. } => vec![*block],
Terminal::For { init, .. } => smallvec![*init],
Terminal::ForOf { init, .. } | Terminal::ForIn { init, .. } => smallvec![*init],
Terminal::DoWhile { loop_block, .. } => smallvec![*loop_block],
Terminal::While { test, .. } => smallvec![*test],
Terminal::Return { .. } | Terminal::Throw { .. } | Terminal::Unreachable { .. } => {
smallvec![]
}
Terminal::Try { block, .. } => smallvec![*block],
Terminal::MaybeThrow { continuation, handler, .. } => {
let mut v = vec![*continuation];
let mut v = smallvec![*continuation];
if let Some(h) = handler {
v.push(*h);
}
v
}
Terminal::Label { block, .. } | Terminal::Sequence { block, .. } => vec![*block],
Terminal::Logical { test, .. } | Terminal::Ternary { test, .. } => vec![*test],
Terminal::Optional { test, .. } => vec![*test],
Terminal::Scope { block, .. } | Terminal::PrunedScope { block, .. } => vec![*block],
Terminal::Label { block, .. } | Terminal::Sequence { block, .. } => smallvec![*block],
Terminal::Logical { test, .. } | Terminal::Ternary { test, .. } => smallvec![*test],
Terminal::Optional { test, .. } => smallvec![*test],
Terminal::Scope { block, .. } | Terminal::PrunedScope { block, .. } => smallvec![*block],
}
}
@@ -63,7 +63,7 @@ pub trait ReactiveFunctionVisitor<'a> {
let inner_func = &self.env().functions[func_id];
let block = &inner_func.body.blocks[&block_id];
let instr_ids: Vec<_> = block.instructions.clone();
let terminal_operands: Vec<Place> = each_terminal_operand(&block.terminal);
let terminal_operands = each_terminal_operand(&block.terminal);
let terminal_id = block.terminal.evaluation_order();
for instr_id in &instr_ids {
@@ -9,11 +9,12 @@ use rustc_hash::{FxHashMap, FxHashSet};
use oxc_diagnostics::OxcDiagnostic;
use oxc_index::IndexSlice;
use smallvec::smallvec;
use crate::diagnostics::ErrorCategory;
use crate::react_compiler_hir::environment::Environment;
use crate::react_compiler_hir::visitors::{
each_instruction_lvalue_ids, each_instruction_value_operand, each_terminal_operand,
PlaceList, each_instruction_lvalue_ids, each_instruction_value_operand, each_terminal_operand,
};
use crate::react_compiler_hir::{
Effect, FunctionId, HirFunction, Identifier, IdentifierId, IdentifierName, InstructionValue,
@@ -204,24 +205,24 @@ fn get_context_reassignment(
// For calls with noAlias signatures, only check the callee/receiver
// (not args) to avoid false positives from callbacks that reassign
// context variables.
let operands: Vec<Place> = match &instr.value {
let operands: PlaceList = match &instr.value {
InstructionValue::CallExpression { callee, .. } => {
if env.has_no_alias_signature(callee.identifier) {
vec![callee.clone()]
smallvec![callee.clone()]
} else {
each_instruction_value_operand(&instr.value, env)
}
}
InstructionValue::MethodCall { receiver, property, .. } => {
if env.has_no_alias_signature(property.identifier) {
vec![receiver.clone(), property.clone()]
smallvec![receiver.clone(), property.clone()]
} else {
each_instruction_value_operand(&instr.value, env)
}
}
InstructionValue::TaggedTemplateExpression { tag, .. } => {
if env.has_no_alias_signature(tag.identifier) {
vec![tag.clone()]
smallvec![tag.clone()]
} else {
each_instruction_value_operand(&instr.value, env)
}