Bottom line first: React Compiler is a major step forward, but it optimizes React's expression and render costs within React's runtime contract.
Compiler-first fine-grained frameworks like Fict pursue a different objective: move more update routing to compile time, then execute updates through dependency propagation at runtime.
This is not a winner-takes-all debate. It is an engineering trade-off across different objective functions.
Fict Repo: https://github.com/fictjs/fict
0. Scope and Rigor
To avoid slogan-level comparisons, this article uses strict boundaries:
- It compares two technical routes:
-
React + React Compiler(preserving React semantics) -
Compiler-first fine-grained(using Fict as the concrete example)
-
- It separates three layers:
- Developer experience (mental overhead and ergonomics)
- Performance model (update path and cost structure)
- Semantic constraints (effects, lifecycle, error handling, consistency)
- Claims about Fict are grounded in this repository's implementation and docs, not idealized marketing descriptions.
1. What React Compiler Actually Solves
React Compiler does not "replace React." It improves React's default performance posture by:
- Inferring dependency boundaries so developers write less manual
useMemoanduseCallback. - Reusing expressions and JSX subtrees more aggressively.
- Raising performance floor while preserving the React programming model.
A conceptual sketch:
function ProductList({ products, filter, onSelect }) {
const cache = getCompilerCache()
const filtered = cache.memo('filtered', [products, filter], () =>
products.filter(p => p.category === filter),
)
const handleClick = cache.memo('handleClick', [onSelect], () => id => onSelect(id))
return cache.memo('listJSX', [filtered, handleClick], () => (
<ul>
{filtered.map(p => (
<ProductItem key={p.id} product={p} onClick={handleClick} />
))}
</ul>
))
}
Key point: this is stronger automatic memoization inside React semantics, not a replacement of Fiber/Hook contracts.
2. What React Compiler Does Not Redefine
A precise statement:
- React Compiler can remove substantial redundant compute and subtree work.
- It does not redefine React into a signal-driven DOM execution engine.
Why:
- React's core abstraction is still component-tree scheduling and commit semantics.
- Compiler optimizations must remain inside React boundaries: hook ordering, effect timing, concurrent scheduling, error boundary behavior.
- Therefore it optimizes "how much work React does," not "what React fundamentally is."
Practical implications:
- React updates are not always full-tree reruns due to bailouts and memoized outputs.
- But the dominant scheduling unit remains component/subtree semantics, not explicit signal graph nodes.
-
React.memowithchildrenis not inherently useless:- It often fails if children are recreated each render.
- It can work when references are stabilized by caller patterns or compiler transforms.
3. What Fict Changes (Based on Current Repository)
Fict's route is compiler-first fine-grained reactivity: compile dependency relationships, then propagate updates through runtime graph nodes.
This is visible in the current codebase:
- Macro contract:
-
$stateand$effectare compile-time macros and throw if executed at runtime (packages/fict/src/index.ts).
-
- Compile-time placement constraints:
- Macro placement is validated; loops/conditionals/nested contexts are restricted according to compiler rules (
packages/compiler/src/index.ts,docs/compiler-spec.md).
- Macro placement is validated; loops/conditionals/nested contexts are restricted according to compiler rules (
- IR and lowering pipeline:
- HIR build, optimization, and lowering stages exist in
packages/compiler/src/ir/*.
- HIR build, optimization, and lowering stages exist in
- Runtime graph model:
- Signal/computed/effect propagation is implemented in
packages/runtime/src/signal.ts.
- Signal/computed/effect propagation is implemented in
- Local list reconciliation still exists:
- Fict avoids whole-tree VDOM diffing, but keyed local reconciliation is implemented (
packages/runtime/src/reconcile.ts,packages/runtime/src/list-helpers.ts).
- Fict avoids whole-tree VDOM diffing, but keyed local reconciliation is implemented (
- Fail-closed guarantee mode is explicit:
-
FICT_STRICT_GUARANTEE=1enforces strict diagnostics in CI (docs/reactivity-guarantee-matrix.md).
-
This corrects two frequent misconceptions:
- "Fict has no reconciliation at all." Not true. It avoids whole-tree VDOM diffing; it still reconciles local keyed list paths.
- "Fict is only syntax sugar over existing runtimes." Not true. It has a dedicated compile-time dataflow and lowering pipeline with a distinct runtime execution model.
4. Cost Model: Same Interaction, Different Work Units
Use a searchable list as an example:
// React + Compiler (conceptual)
function SearchList({ items }) {
const [query, setQuery] = useState('')
const [sortBy, setSortBy] = useState('name')
const filtered = items.filter(item => item.name.toLowerCase().includes(query.toLowerCase()))
const sorted = [...filtered].sort((a, b) => a[sortBy].localeCompare(b[sortBy]))
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="name">Name</option>
<option value="date">Date</option>
</select>
<p>{sorted.length} results</p>
<ul>
{sorted.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
)
}
// Fict (conceptual)
function SearchList({ items }) {
let query = $state('')
let sortBy = $state('name')
const filtered = items.filter(item => item.name.toLowerCase().includes(query.toLowerCase()))
const sorted = [...filtered].sort((a, b) => a[sortBy].localeCompare(b[sortBy]))
return (
<div>
<input value={query} onInput={e => (query = e.target.value)} />
<select value={sortBy} onChange={e => (sortBy = e.target.value)}>
<option value="name">Name</option>
<option value="date">Date</option>
</select>
<p>{sorted.length} results</p>
<ul>
{sorted.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
)
}
Conceptual complexity comparison:
- React path:
-
O(rendered-subtree + diff-subtree + patch), reduced by compiler/bailouts.
-
- Fict path:
-
O(changed-sources + affected-computed/effects + affected-bindings + local-list-reconcile).
-
The correct conclusion is not "always faster/slower":
- Fine-grained tends to win when changes are local, dependency graph is sparse, and list size is large.
- React Compiler is often sufficient when bottlenecks are I/O dominated or UI compute is moderate.
5. Semantic Trade-offs (The Part People Skip)
Performance-only comparisons are incomplete. Semantic behavior determines production risk.
5.1 Effects Are Not Mechanical One-to-One Replacements
useEffect and $effect are not drop-in equivalents:
- Fict
$effectis compiler-constrained and analyzed in a macro-based model. - Dependency collection semantics differ from React dependency-array workflows.
- Less manual dependency bookkeeping reduces a class of developer mistakes, but correctness partially shifts toward compiler analyzability and guarantee coverage.
5.2 Control Flow and Dynamic Shapes Need Explicit Validation
High-risk migration areas:
- Effect cleanup timing under control-flow branch switching.
- Closure reads in event chains and async callbacks.
- Cancellation/race handling in async flows.
- List identity behavior under unstable key quality.
5.3 "No Free Ecosystem Inheritance"
React's long-lived advantages are structural:
- Ecosystem breadth and battle-tested framework integrations.
- Mature concurrent scheduling and tooling culture.
- Broad operational knowledge in teams.
Compiler-first routes must independently harden:
- DevTools observability
- SSR/Hydration/Resume stability
- Error boundary and suspense consistency
- Third-party integration contracts
6. Side-by-Side Decision Matrix
| Dimension | React + Compiler | Fict / Compiler-First Fine-Grained |
|---|---|---|
| Primary objective | Reduce React performance tuning overhead | Lower runtime update upper bound |
| Core mechanism | Automatic memoization + bailouts | Compile-time dependency graph + runtime propagation |
| Dominant update unit | Component/subtree scheduling | Source/computed/binding-level propagation |
| List behavior | Reconciliation in React tree semantics | Local keyed reconciliation |
| Migration cost | Low to medium (keep React mental model) | Medium to high (compiler constraints + semantic verification) |
| Main risk | Performance predictability tied to component structure | Semantic edge cases and compiler coverage boundaries |
Important framing: this is objective-function selection, not a "modernity contest."
7. What a Defensible Technical Article Must Include
If the goal is engineering evidence rather than opinion, include at least:
- Public benchmark method:
- Dataset, warm/cold separation, hardware/software baselines.
- Metric decomposition:
- Render/commit timing, DOM mutations, GC, memory peak.
- Counterexample sets:
- Cases where React is better; cases where fine-grained is better.
- Semantic equivalence tests:
- Cleanup, error boundaries, async races, fallback paths.
- Migration cost quantification:
- Changed LOC, defect categories, regression count, fix lead time.
Without this, conclusions remain high-quality perspective, not reproducible engineering claims.
8. Reproducible Evaluation Appendix
Use repository-native commands only.
- Environment baseline:
- Node
>=20 - pnpm
>=9 - OS/CPU/RAM
- Browser version (for benchmark runs)
- Node
- Repository health:
pnpm install
pnpm build
pnpm test
pnpm typecheck
pnpm lint
- Strict guarantee gate:
FICT_STRICT_GUARANTEE=1 pnpm build
- Framework benchmark flow:
pnpm perf:data
pnpm perf:results
- Runtime stress:
pnpm stress:runtime
pnpm stress:runtime:long
- Compiler optimization regression guard:
pnpm bench:optimizer:guard
- Reporting minimums:
- Scenario and data scale (
N, update rate, list size) - Median + IQR from at least 5 runs
- Error logs and failure sample links
- Scenario and data scale (
9. Migration Readiness Checklist
Before adopting a compiler-first route, verify:
- You are solving a front-end CPU update bottleneck, not primarily network/back-end latency.
- You can run strict guarantee mode in CI (
FICT_STRICT_GUARANTEE=1). - You have semantic regression coverage for effects, async races, and error boundaries.
- Team conventions can absorb macro constraints and compile-time diagnostics.
- You have a rollback strategy for fallback-path regressions.
When to prefer React + Compiler first:
- Large existing React codebase with narrow migration window.
- Heavy dependency on mainstream React ecosystem/runtime assumptions.
- Performance gains needed are incremental, not architectural.
When Fict-like route is justified:
- CPU update path is a top product bottleneck.
- Large/interactive local updates dominate user experience.
- Team is willing to trade some migration complexity for lower runtime update ceiling.
10. Final Conclusion
React Compiler matters because it turns performance tuning expertise into compiler defaults inside React semantics.
Fict-like compiler-first systems matter because they can replace rerun-and-compare execution with dependency-driven propagation for many update paths.
Both are valid. They optimize different costs:
- React Compiler answers:
- How far can we push automatic performance within React's existing contract?
- Compiler-first fine-grained answers:
- If dataflow is known at compile time, how much runtime tree-level comparison can we avoid?
A serious engineering decision should not declare a universal winner.
It should define boundaries, publish evidence, and make trade-offs explicit.
Top comments (0)