React nu volledig in Rust
De v6.1.0-release van @vitejs/plugin-react introduceerde "experimentele native React Compiler-ondersteuning". Je kunt dit activeren door { compiler: true } mee te geven aan de plugin in je Vite-configuratie. Voor wie de Vite React-plugin niet kan gebruiken (bijvoorbeeld wanneer je, net als wij, React Router in framework-modus gebruikt), is @acusti/vite-plugin-react-compiler een minimale Vite-plugin om je codebase te compileren, ongeacht de rest van je build-pipeline.
Snellere builds = gelukkigere developers + goedkopere CI
Het belangrijkste kenmerk van deze wijziging is de snelheidswinst. Volgens Boshen, de projectleider van oxc:
"Het is meer dan 10 keer sneller dan Babel in onze voorlopige benchmark."
Wij zagen een versnelling van meer dan 17×; 1.036 bestanden gingen van 14,3 seconden bij een build met Babel naar 0,81 seconden native (single-threaded). Dit is voor ons van groot belang omdat CI-gebruik en GitHub Actions-minuten een aanzienlijke kostenpost zijn geworden door de snelheid van veranderingen die door agent-assisted softwareontwikkeling wordt veroorzaakt. Bovendien is wachten op CI frustrerend en legt het extra druk op ons reeds gefragmenteerde takenbeheer.
Let op dat deze versnellingen alleen gelden voor het compilergedeelte van het buildproces. Waarschijnlijk gebeuren er tijdens de build nog veel andere zaken, waardoor de totale verbetering van de buildtijd minder dramatisch zal zijn. In ons geval werd de build ongeveer 2,4× sneller (van 22,1s naar 9,3s).
Hoe zit het met de beperkingen van de React Compiler?
Hoewel snelheid in de schijnwerpers staat, ben ik enthousiaster over de voordelen van de nieuwste versie van de React Compiler. Deze heeft al enkele aanzienlijke beperkingen in de JavaScript-ondersteuning opgelost die nog aanwezig waren in v1.0 van de Babel-gebaseerde React Compiler.
Dit omvat ondersteuning voor alle soorten conditionele logica in try/catch-blokken, wat voor velen een blokkade vormde bij de initiële stabiele 1.0-release. Een andere mooie fix die vlak voor het schrijven van dit artikel is doorgevoerd, is de ondersteuning voor het opnieuw toewijzen van een destructured component prop die vervolgens wordt gebruikt in een nested closure, bijvoorbeeld:
export default function Foo({ value }: { value: null | string }) {
value = value ?? "this is a fallback";
return <button onClick={() => console.log(value)}>{value}</button>;
}
Dit werd voorheen overgeslagen, maar wordt nu volledig ondersteund. Een derde veelvoorkomend patroon dat in de Babel-compiler leidde tot bailouts en nu wordt ondersteund, zijn computed object property keys, bijvoorbeeld:
import { clsx } from "clsx";
export default function Header({ itemCount }: { itemCount: number }) {
return (
<header className={clsx({ [`items-${itemCount}`]: itemCount > 0 })}>
{/* ... */}
</header>
);
}
Deze fixes betekenen dat de nieuwe versie de compatibiliteit van de compiler in onze app uitbreidt met zeven extra functies: vijf dankzij de try/catch-verbetering en twee dankzij de computed object property keys.
Om duidelijk te zijn: er zijn nog steeds beperkingen. De twee patronen die ik ben tegengekomen die ervoor zorgen dat de compiler een component of hook nog steeds overslaat, zijn een throw vanuit een try-blok en logische toewijzingsoperatoren (??=, &&=, ||=). Maar door de Rust-compiler te gebruiken, ontvang je deze fixes zodra ze beschikbaar komen. Dat is niet het geval als je vastzit aan de doodlopende Babel-gebaseerde compiler.
Consistentie in de toolchain voorkomt gaten in de dekking
De laatste reden waarom ik enthousiast ben over de overstap, is dat mijn volledige toolchain nu dezelfde versie van de React Compiler gebruikt met equivalente ondersteuning van functies.
Toen ik de React Compiler-ondersteuning van Oxlint adopteerde terwijl ik voor mijn build nog een eerdere versie van de React Compiler gebruikte, diende ik een foutieve issue in bij oxc. Dit ging over de destructured component prop bailout die ik eerder beschreef; de component werd niet geoptimaliseerd tijdens de build, maar triggerde ook geen lint-fout. Ik dacht dat er een disconnect was tussen de linter en de compiler-output. Het bleek echter dat Oxlint oxc-transform-react v0.145.0 gebruikte, die dat patroon ondersteunt, terwijl ik testte met v0.144.0 van hetzelfde pakket.
Nu gebruiken de linter en de build exact dezelfde React Compiler, met dezelfde verbeteringen en beperkingen, waardoor we ons geen zorgen meer hoeven te maken over niet-gecompileerde componenten die in onze productie-build terechtkomen.
Hoe te gebruiken
Gebruik van @vitejs/plugin-react
Als je Vite v8+ gebruikt, betekent het overzetten van een bestaande React Vite-build naar de native React Compiler in feite dat je de configuratie vereenvoudigt. De huidige Babel-gebaseerde instructies van react.dev schrijven voor om het volgende uit te voeren:
npm install -D @rolldown/plugin-babel
Met de volgende Vite-configuratie:
// vite.config.js
import { defineConfig } from "vite";
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
import babel from "@rolldown/plugin-babel";
export default defineConfig({
plugins: [react(), babel({ presets: [reactCompilerPreset()] })],
});
Overstappen naar native betekent dat je onnodige configuratie verwijdert. Je voert uit:
npm install -D oxc-transform-react
En vereenvoudigt je Vite-configuratie:
// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react({ compiler: true })],
});
Dit betekent ook dat je @rolldown/plugin-babel uit je package.json dev-dependencies kunt verwijderen.
Geen gebruik van @vitejs/plugin-react (bijv. React Router Framework Mode)
Voor codebases die React Router in framework-modus gebruiken, is de overstap iets anders. React Router heeft zijn eigen Vite-plugin die in plaats van de Vite React-plugin moet worden uitgevoerd. Waar je voorheen het volgende moest doen:
npm install -D vite-plugin-babel babel-plugin-react-compiler @babel/preset-typescript
Met de volgende Vite-configuratie:
// vite.config.js
import { defineConfig } from "vite";
import babel from "vite-plugin-babel";
import { reactRouter } from "@react-router/dev/vite";
const ReactCompilerConfig = {
/* optionele config indien aanwezig */
};
export default defineConfig({
plugins: [
reactRouter(),
babel({
babelConfig: {
presets: ["@babel/preset-typescript"], // indien je TypeScript gebruikt
plugins: [["babel-plugin-react-compiler", ReactCompilerConfig]],
},
exclude: /node_modules/,
include: /\.[jt]sx?$/,
}),
],
});
Kun je nu vite-plugin-babel, babel-plugin-react-compiler en @babel/preset-typescript volledig verwijderen en in plaats daarvan installeren:
npm install -D @acusti/vite-plugin-react-compiler
En je configuratie vereenvoudigen naar:
// vite.config.js
import { defineConfig } from "vite";
import reactCompiler from "@acusti/vite-plugin-react-compiler";
import { reactRouter } from "@react-router/dev/vite";
export default defineConfig({
plugins: [reactRouter(), reactCompiler()],
// of, als je een aangepaste compiler-configuratie moet meegeven:
// reactCompiler({ compiler: { /* je bestaande ReactCompilerConfig */ } })
});
Eenvoudiger, sneller en krachtiger. Proost daarop.
Groetjes,