@fcannizzaro/streamdeck-react

React Compiler

Automatically optimize re-renders with React Compiler.

React Compiler is an optional build-time optimization that automatically memoizes your components, eliminating the need for manual useMemo, useCallback, and React.memo.

Why It Matters Here

In a standard React DOM app, unnecessary re-renders cause DOM mutations -- relatively cheap operations. In @fcannizzaro/streamdeck-react, every re-render triggers an expensive pipeline:

  1. VNode tree reconstruction
  2. Layout computation (Takumi / Rust)
  3. Rasterization to PNG (Rust native binding)
  4. Image push to Stream Deck hardware

React Compiler prevents unnecessary re-renders at the source, before the VNode tree is even touched. This is especially valuable for components using useTick, useInterval, or frequent state updates.

Setup via Scaffolding

The easiest way is to enable it during project creation:

npm create streamdeck-react@latest

The CLI will ask "Use React Compiler?" during setup. You can also pass it as a flag:

npm create streamdeck-react@latest --react-compiler true

When enabled, the generated config uses babel-plugin-react-compiler -- via @rollup/plugin-babel for Rollup or @rolldown/plugin-babel for Vite 8.

Manual Setup

Rollup

If you have an existing Rollup project using esbuild, replace it with Babel:

1. Swap Dependencies

Remove esbuild:

bun remove rollup-plugin-esbuild

Install Babel + React Compiler:

bun add -d @rollup/plugin-babel @babel/core @babel/preset-typescript @babel/preset-react babel-plugin-react-compiler

2. Update Rollup Config

Replace the esbuild import and plugin:

// rollup.config.mjs
import { builtinModules } from "node:module";
import { babel } from "@rollup/plugin-babel";
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import json from "@rollup/plugin-json";
import { streamDeckReact } from "@fcannizzaro/streamdeck-react/rollup";

const PLUGIN_DIR = "com.example.my-plugin.sdPlugin";
const builtins = new Set(builtinModules.flatMap((m) => [m, `node:${m}`]));

export default {
  input: "src/plugin.ts",
  output: {
    file: `${PLUGIN_DIR}/bin/plugin.mjs`,
    format: "es",
    sourcemap: true,
    inlineDynamicImports: true,
  },
  external: (id) => builtins.has(id),
  plugins: [
    resolve({ preferBuiltins: true }),
    commonjs(),
    json(),
    babel({
      babelHelpers: "bundled",
      extensions: [".js", ".jsx", ".ts", ".tsx"],
      exclude: "**/node_modules/**",
      plugins: ["babel-plugin-react-compiler"],
      presets: ["@babel/preset-typescript", ["@babel/preset-react", { runtime: "automatic" }]],
    }),
    streamDeckReact({
      targets: [{ platform: "darwin", arch: "arm64" }],
    }),
  ],
};

Key differences from the esbuild config:

  • babel() replaces esbuild()
  • exclude: '**/node_modules/**' prevents Babel from processing dependencies
  • babel-plugin-react-compiler must be listed in plugins (runs first)
  • @babel/preset-typescript and @babel/preset-react handle TypeScript and JSX

Vite 8 (Rolldown)

If you have an existing Vite 8 project, add the Babel plugin:

1. Install Dependencies

bun add -d @rolldown/plugin-babel @babel/core babel-plugin-react-compiler

2. Update Vite Config

Add the babel plugin with reactCompilerPreset():

// vite.config.ts
import { builtinModules } from "node:module";
import { resolve } from "node:path";
import { defineConfig, esmExternalRequirePlugin } from "vite";
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
import babel from "@rolldown/plugin-babel";
import { streamDeckReact } from "@fcannizzaro/streamdeck-react/vite";

const PLUGIN_DIR = "com.example.my-plugin.sdPlugin";
const builtins = builtinModules.flatMap((m) => [m, `node:${m}`]);

export default defineConfig({
  resolve: {
    conditions: ["node"],
  },
  plugins: [
    esmExternalRequirePlugin({ external: builtins }),
    react(),
    // @ts-expect-error — @rolldown/plugin-babel types incorrectly mark inherited babel fields as required
    await babel({
      presets: [reactCompilerPreset()],
    }),
    streamDeckReact({
      uuid: "com.example.my-plugin",
      targets: [{ platform: "darwin", arch: "arm64" }],
    }),
  ],
  build: {
    target: "node20",
    outDir: resolve(PLUGIN_DIR, "bin"),
    emptyOutDir: false,
    sourcemap: true,
    minify: false,
    lib: {
      entry: resolve("src/plugin.ts"),
      formats: ["es"],
      fileName: () => "plugin.mjs",
    },
    rolldownOptions: {
      output: {
        codeSplitting: false,
      },
    },
  },
});

Key differences from the non-compiler Vite config:

  • Import reactCompilerPreset from @vitejs/plugin-react
  • Add @rolldown/plugin-babel with reactCompilerPreset() after react()

Verifying It Works

After building, check the output bundle for the compiler's runtime marker:

grep "react.memo_cache_sentinel" com.example.my-plugin.sdPlugin/bin/plugin.mjs

If the compiler is active, you'll see Symbol.for("react.memo_cache_sentinel") in the output.

Opting Out Per Component

If a specific component causes issues after compilation, use the "use no memo" directive:

function ProblematicComponent() {
  "use no memo";
  // This component won't be optimized by the compiler
}

No Code Changes Required

React Compiler works with standard React patterns. Your existing components, hooks, and state management (Zustand, Jotai, TanStack Query) all work without modification.

On this page