React Compiler + shadcn/ui: Frontend Development in the Auto-Optimization Era
At 3 AM, I stared at React DevTools, watching a shadcn Data Table component re-render on every scroll—even though the props hadn’t changed at all. I had manually written over 20 useMemo hooks, but still missed some edge cases. At that moment, I thought: if only there was a tool that could automatically handle all these optimizations for me.
Two months later, React Compiler v1.0 was officially released. I no longer have to struggle with whether to add useMemo to a function, or worry about missing a useCallback. The compiler handles everything during build time.
But what does this mean for shadcn/ui projects? After enabling Compiler, do we still need those manually written memoizations? Will shadcn components have compatibility issues? This article shares my practical experience from the past few months.
TL;DR
1. What is React Compiler?
Honestly, React Compiler isn’t some “revolutionary” thing—it’s more like an automation tool that helps you do performance optimizations you’d otherwise write manually.
Previously, when writing React, every time you encountered performance issues, you had to manually add useMemo, useCallback, React.memo. Missing one could cause the entire page to lag. And these memoization logic can be quite complex—you need to analyze dependencies, judge edge cases, and consider whether you’re over-optimizing.
React Compiler’s approach is: since these optimization logic are pattern-based, let the compiler analyze them during build time and automatically insert appropriate memoizations.
For example, when writing a Data Table before, I had to do this:
// Manual optimization version (tedious)
const columns = useMemo(() => [
{
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => row.original.name,
},
// ... more column definitions
], []); // Need to maintain dependency array yourself
const handleRowClick = useCallback((row) => {
console.log('Clicked:', row);
}, []);
After enabling Compiler, this code can be deleted directly:
// Compiler auto-optimization version (clean)
const columns = [
{
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => row.original.name,
},
];
const handleRowClick = (row) => {
console.log('Clicked:', row);
};
The compiler analyzes these functions’ dependencies during build time and automatically determines whether to memoize. You no longer have to struggle with “should I add useMemo” questions.
The official description is: “build-time performance optimization”. Simply put, the compiler does React.memo’s work for you.
2. Enabling React Compiler: Three Methods
If you’re using Next.js 16, congratulations—Compiler is already built-in. Other build tools need additional configuration.
Method 1: Next.js 16 (Most Convenient)
Next.js 16 integrates React Compiler by default. Just add one line in next.config.js:
// next.config.js
const nextConfig = {
experimental: {
reactCompiler: true, // Enable Compiler
},
};
export default nextConfig;
All React components in the project will be automatically optimized. No code changes needed.
Method 2: Vite + React Compiler Plugin
Vite projects need to install a Babel plugin:
npm install --save-dev babel-plugin-react-compiler
Then configure in vite.config.ts:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: [
['babel-plugin-react-compiler', {
// Optional: specify compilation mode
// 'mode': 'optimize'
}],
],
},
}),
],
});
Vite will automatically apply Compiler during build time.
Method 3: Standalone Babel Configuration (For Other Tools)
If you’re using webpack, Rollup, or other build tools, you can add the plugin directly in Babel configuration:
// .babelrc or babel.config.json
{
"plugins": [
["babel-plugin-react-compiler"]
]
}
Any build tool using Babel can support Compiler.
3. shadcn/ui + React Compiler: Practical Experience
Let’s talk about actual results. I used Compiler to refactor a shadcn/ui admin project with about 40+ components. The overall experience was good, but there are some details to watch out for.
Scenario 1: Dialog Component Re-rendering
shadcn’s Dialog component is a typical pure component—when props don’t change, render result doesn’t change. But when manually optimizing before, I often forgot to add useCallback to Dialog’s onOpenChange.
After enabling Compiler, this type of problem was automatically solved. Compiler can identify that onOpenChange is a stable function (no external dependencies) and automatically memoize it.
Tested it out—when opening Dialog, the parent component no longer re-renders. Before, every time Dialog opened, the parent component would re-render (because onOpenChange was a new function each time).
Scenario 2: Form + Zod Validation
shadcn’s Form component uses React Hook Form + Zod. When writing validation rules before, I had to do this:
// Manual optimization version
const formSchema = useMemo(() => z.object({
username: z.string().min(2, 'At least 2 characters'),
email: z.string().email('Invalid email format'),
}), []);
const onSubmit = useCallback((values) => {
console.log(values);
}, []);
Now all these useMemo/useCallback are deleted, just write:
// Compiler auto-optimization version
const formSchema = z.object({
username: z.string().min(2, 'At least 2 characters'),
email: z.string().email('Invalid email format'),
});
const onSubmit = (values) => {
console.log(values);
};
Compiler automatically determines whether these functions are stable. If Zod schema returns the same object each time (no external variable dependencies), Compiler will automatically memoize.
Scenario 3: Data Table Rendering
shadcn’s Data Table is based on TanStack Table. This scenario is most prone to performance issues—column definitions, sorting functions, filtering logic, everywhere might need manual optimization.
After enabling Compiler, column definition functions and event handlers are automatically optimized. Tested rendering count:
- Manual optimization version: during scrolling, table component renders 15 times/second (normal)
- Unoptimized version: during scrolling, table component renders 45 times/second (poor performance)
- Compiler auto-optimization version: same as manual optimization, 15 times/second
Effect is basically identical. And deleted 30+ lines of manual optimization code, readability improved a lot.
Bundle Size Impact
Many people worry whether Compiler will increase bundle size. Actual testing shows minimal impact—because Compiler’s optimization logic is inserted during build time, no additional runtime code is added.
Comparison:
- Without Compiler: bundle 142KB
- With Compiler: bundle 144KB
Added 2KB, mainly memoization logic inserted by compiler. But gained deletion of manual useMemo/useCallback (these were already in bundle), overall impact is minimal.
4. Migration Notes: Pitfalls to Avoid
Compiler isn’t perfect, there are several pitfalls to watch out for during migration.
1. Naming Conventions Are Important
Compiler relies on variable naming to infer dependency relationships. If your code is written like this:
// ❌ Compiler may analyze inaccurately
function MyComponent(props) {
return <div>{props.data.name}</div>;
}
Compiler may not correctly identify props.data dependencies. Suggest changing to:
// ✅ Clear naming
function MyComponent({ data }) {
return <div>{data.name}</div>;
}
This way Compiler can more accurately judge data’s dependency relationships.
Actually, this convention is mentioned in React official docs—destructuring props makes code clearer and more favorable for Compiler analysis.
2. ESLint Rules Will Change
If your project has react-hooks/exhaustive-deps rule, after enabling Compiler, this rule’s reports will change.
Before, missing useMemo dependencies, ESLint would report errors. Now Compiler automatically handles dependencies, this rule becomes less important.
Suggest adjusting ESLint configuration, downgrade exhaustive-deps rule to “warn” or turn it off directly. Because Compiler already handles dependencies for you, this rule becomes “noise”.
// .eslintrc
{
"rules": {
"react-hooks/exhaustive-deps": "off" // Compiler has handled dependencies
}
}
3. Third-party Library Compatibility
Some third-party libraries may not be compatible with Compiler. Especially those with complex side-effect logic inside.
If after enabling Compiler, build errors or runtime issues occur, you can troubleshoot like this:
- First check error messages—usually some component’s naming or logic doesn’t comply with Compiler spec
- Disable Compiler for that component (add
'use no memo'annotation) - Gradually troubleshoot, find incompatible components
I encountered a third-party drag library that wasn’t compatible. Solution was to disable Compiler for that drag component:
'use no memo'; // Tell Compiler: don't optimize this component
function DraggableList() {
// Drag logic...
}
This way Compiler will skip this component, no automatic optimization.
4. When to Disable Compiler?
Most components work fine with Compiler enabled. But several scenarios suggest disabling:
- Complex side-effect logic: like timers, animations, direct DOM manipulation, Compiler may analyze inaccurately
- Third-party library incompatibility: some libraries have complex internal logic, Compiler may misjudge
- Performance degradation: in some extreme cases, Compiler’s memoization may be excessive, actually increasing memory usage (this situation is rare)
Disable method is adding 'use no memo' annotation to let Compiler skip that component.
5. From Manual to Automatic: My Migration Log
Let’s talk about actual migration process. Project is an admin system with 40+ shadcn/ui components.
Code Before Migration
Approximately 50+ manually written useMemo/useCallback. Data Table part most complex—column definitions, sorting, filtering, everywhere manually optimized.
Code looked quite tedious:
// Data Table before migration (manual optimization)
const columns = useMemo(() => [
{ accessorKey: 'id', header: 'ID' },
{ accessorKey: 'name', header: 'Name' },
// ...more columns
], []);
const sorting = useMemo(() => [{ id: 'name', desc: true }], []);
const handleSortingChange = useCallback((updater) => {
setSorting(updater);
}, []);
Code After Migration
After enabling Compiler, deleted all manual optimizations:
// Data Table after migration (Compiler auto-optimization)
const columns = [
{ accessorKey: 'id', header: 'ID' },
{ accessorKey: 'name', header: 'Name' },
];
const sorting = [{ id: 'name', desc: true }];
const handleSortingChange = (updater) => {
setSorting(updater);
};
Code is cleaner, readability also improved a lot. Before, reading code, had to analyze whether each useMemo’s dependencies were correct; now don’t worry about this problem.
Performance Comparison
Tested Lighthouse scores:
- Before migration: Performance 82, LCP 1.8s
- After migration: Performance 85, LCP 1.6s
Slightly better. Mainly deleted some unnecessary manual optimizations (some useMemo were actually redundant), Compiler only inserts memoization where really needed.
Also tested actual render time:
- Manual optimization version: Data Table scrolling, average render time 12ms
- Compiler version: average render time 10ms
Similar, even slightly better. Shows Compiler’s optimization logic is reasonable.
Team Feedback
After migration completed, asked several colleagues’ feelings:
- “Don’t have to struggle with whether to memo anymore, quite worry-free”
- “After deleting those useMemo, code looks much cleaner”
- “Had a component that wasn’t compatible, added ‘use no memo’ and solved it, okay”
Overall feedback is positive. Everyone feels much less mental burden—don’t have to think “should this function be memoized” every time writing code.
Summary
React Compiler is an important update in React ecosystem. For shadcn/ui projects, enabling Compiler’s benefits are clear:
- Automatic performance optimization: no need to manually write useMemo/useCallback, Compiler handles it during build time
- Simplified code: delete manual optimization code, readability improves
- Reduced mental burden: don’t have to struggle with “should I memoize” every time
When migrating, note several points:
- Naming conventions: destructure props, avoid
props.datastyle writing - ESLint configuration: adjust exhaustive-deps rule
- Third-party library compatibility: if issues, use
'use no memo'to disable
Suggest trying in Next.js 16 projects first—ready out of the box, simplest configuration. Other build tools can reference Vite’s configuration method, gradually migrate.
Honestly, after using Compiler, writing React feels different from before—more like writing “normal” JavaScript, don’t have to constantly think about performance optimization details. This feeling is quite nice.
FAQ
FAQ
Will React Compiler increase bundle size?
Are shadcn/ui components compatible with React Compiler?
After enabling Compiler, should manually written useMemo/useCallback be deleted?
What scenarios need to disable Compiler?
What naming requirements does Compiler have?
How to enable Compiler in Next.js 16 and Vite projects?
9 min read · Published on: Mar 31, 2026 · Modified on: Mar 31, 2026
Related Posts
Astro + Tailwind: Configuring Island Components and Global Styles Without Conflicts
Astro + Tailwind: Configuring Island Components and Global Styles Without Conflicts
Next.js App Router + shadcn/ui: A Guide to Mixing Server and Client Components
Next.js App Router + shadcn/ui: A Guide to Mixing Server and Client Components
shadcn/ui and Radix: How to Maintain Accessibility When Customizing Components

Comments
Sign in with GitHub to leave a comment