After building several Chrome extensions using both React and Next.js, I've learned that while both can work, they serve very different use cases. Here's my technical breakdown of when and how to use each approach.
The Core Difference
React gives you complete control over the build process and is naturally suited for Chrome extensions. Next.js is a full-stack framework that requires significant workarounds to fit into the extension architecture.
React Approach: The Natural Fit
my-extension/
├── public/
│ ├── manifest.json
│ ├── icons/
│ └── content.css
├── src/
│ ├── popup/
│ │ ├── Popup.jsx
│ │ └── index.js
│ ├── content/
│ │ ├── ContentScript.jsx
│ │ └── index.js
│ ├── background/
│ │ └── background.js
│ └── options/
│ ├── Options.jsx
│ └── index.js
├── webpack.config.js
└── package.json
Webpack Configuration
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
entry: {
popup: './src/popup/index.js',
content: './src/content/index.js',
background: './src/background/background.js',
options: './src/options/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react']
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new CopyPlugin({
patterns: [
{ from: 'public', to: '.' }
]
})
]
};
Manifest.json
{
"manifest_version": 3,
"name": "My React Extension",
"version": "1.0",
"action": {
"default_popup": "popup.html"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"css": ["content.css"]
}
],
"background": {
"service_worker": "background.js"
},
"permissions": ["storage", "activeTab"]
}
Real Example: Popup Component
// src/popup/Popup.jsx
import React, { useState, useEffect } from 'react';
const Popup = () => {
const [currentUrl, setCurrentUrl] = useState('');
const [savedUrls, setSavedUrls] = useState([]);
useEffect(() => {
// Get current tab URL
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
setCurrentUrl(tabs[0].url);
});
// Load saved URLs
chrome.storage.sync.get(['savedUrls'], (result) => {
setSavedUrls(result.savedUrls || []);
});
}, []);
const saveUrl = () => {
const newUrls = [...savedUrls, currentUrl];
setSavedUrls(newUrls);
chrome.storage.sync.set({ savedUrls: newUrls });
};
return (
<div style={{ width: '300px', padding: '20px' }}>
<h3>URL Bookmarker</h3>
<p>Current: {currentUrl}</p>
<button onClick={saveUrl}>Save URL</button>
<div>
<h4>Saved URLs:</h4>
{savedUrls.map((url, index) => (
<div key={index} style={{ marginBottom: '8px' }}>
<a href={url} target="_blank" rel="noopener noreferrer">
{url.substring(0, 50)}...
</a>
</div>
))}
</div>
</div>
);
};
export default Popup;
Content Script Integration
// src/content/ContentScript.jsx
import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';
const FloatingWidget = () => {
const [isVisible, setIsVisible] = useState(false);
const [selectedText, setSelectedText] = useState('');
useEffect(() => {
const handleSelection = () => {
const selection = window.getSelection().toString();
if (selection) {
setSelectedText(selection);
setIsVisible(true);
} else {
setIsVisible(false);
}
};
document.addEventListener('mouseup', handleSelection);
return () => document.removeEventListener('mouseup', handleSelection);
}, []);
if (!isVisible) return null;
return (
<div style={{
position: 'fixed',
top: '20px',
right: '20px',
background: 'white',
border: '1px solid #ccc',
borderRadius: '8px',
padding: '16px',
zIndex: 10000,
boxShadow: '0 4px 12px rgba(0,0,0,0.15)'
}}>
<h4>Selected Text</h4>
<p>{selectedText}</p>
<button onClick={() => setIsVisible(false)}>Close</button>
</div>
);
};
// Inject the component
const container = document.createElement('div');
document.body.appendChild(container);
const root = createRoot(container);
root.render(<FloatingWidget />);
Next.js Approach: Fighting the Framework
The Challenge
Next.js is designed for server-side rendering and full-stack applications, not browser extensions. You'll need extensive configuration changes.
Modified next.config.js
```javascript/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
trailingSlash: true,
skipTrailingSlashRedirect: true,
distDir: 'dist',
images: {
unoptimized: true
},
webpack: (config, { isServer }) => {
if (!isServer) {
// Disable code splitting for extensions
config.optimization.splitChunks = false;
config.optimization.runtimeChunk = false;
// Multiple entry points for extension components
config.entry = {
popup: './pages/popup.js',
content: './pages/content.js',
background: './pages/api/background.js'
};
}
return config;
}
};
module.exports = nextConfig;
### Project Structure Issues
my-nextjs-extension/
├── pages/
│ ├── popup.js // Not a natural fit
│ ├── content.js // Awkward structure
│ └── api/
│ └── background.js // API routes don't work in extensions
├── components/
├── styles/
├── public/
│ └── manifest.json
└── next.config.js
### Real Example: Popup with Next.js
```jsx
// pages/popup.js
import { useState, useEffect } from 'react';
import Head from 'next/head';
export default function Popup() {
const [data, setData] = useState(null);
useEffect(() => {
// Next.js router doesn't work in extensions
// You lose many Next.js benefits here
if (typeof chrome !== 'undefined') {
chrome.storage.sync.get(['data'], (result) => {
setData(result.data);
});
}
}, []);
return (
<>
<Head>
<title>Extension Popup</title>
{/* Most Next.js head features don't work in extensions */}
</Head>
<div>
<h1>Popup Content</h1>
<p>Data: {data}</p>
{/* Next.js Image component doesn't work */}
{/* Next.js Link component doesn't work */}
{/* Next.js router doesn't work */}
</div>
</>
);
}
Performance Comparison
Bundle Size Analysis
React Setup:
- Popup bundle: ~45KB (with React + your code)
- Content script: ~50KB (React + DOM manipulation)
- Background script: ~2KB (vanilla JS)
- Total: ~97KB
Next.js Setup:
- Popup bundle: ~180KB (Next.js runtime + React + your code)
- Content script: ~200KB (Next.js overhead even when unused)
- Background script: ~15KB (Next.js API route overhead)
- Total: ~395KB
Load Time Impact
Chrome extensions load on every page visit. The 4x larger bundle size with Next.js creates noticeable performance degradation, especially on slower devices.
Development Experience
React Pros:
- Full control over build process
- Natural extension architecture
- Smaller bundles
- All Chrome APIs work seamlessly
- Hot reloading works with proper setup
- Easy debugging
React Cons:
- More initial setup required
- Need to configure Webpack/build tools
- No built-in routing (though extensions rarely need it)
Next.js Pros:
- Familiar development environment if you're already using Next.js
- Built-in TypeScript support
- Good development tooling
Next.js Cons:
- Most Next.js features don't work (SSR, API routes, Image optimization, routing)
- Larger bundle sizes
- Complex configuration workarounds
- Fighting framework assumptions
- Debugging can be more difficult due to framework overhead
Real-World Use Cases
When to Choose React:
- Performance-critical extensions (content scripts that run on every page)
- Extensions with complex UI (options pages, popup interfaces)
- Long-term maintenance projects
- Team unfamiliar with Next.js but comfortable with React
When Next.js Might Work:
- Prototyping existing Next.js components as extension features
- Teams heavily invested in Next.js ecosystem
- Extensions with minimal performance requirements
- **Short-term projects **where setup time matters more than optimization
Migration Experience
I migrated a productivity extension from Next.js to React after encountering several issues:
Problems Encountered:
- Bundle size: Users complained about slow page loads
- Hot reloading: Inconsistent behavior in development
- Chrome API integration: Next.js service worker conflicts
- Build complexity: Constant webpack configuration adjustments
Migration Results:
- 75% reduction in bundle size
- 40% faster extension load times
- Eliminated 3 production bugs related to Next.js routing
- Reduced build time by 60%
My Recommendation
Use React for Chrome extensions. The natural fit, performance benefits, and reduced complexity far outweigh any initial setup overhead. Next.js is an excellent framework, but it's solving problems that don't exist in the extension context while creating new ones.
Starter Template Structure
// Quick setup with Create React App ejected
npx create-react-app my-extension
cd my-extension
npm run eject
Final Thoughts
While both approaches can technically work, React provides a much more natural development experience for Chrome extensions. The performance implications alone make React the clear choice for production extensions, especially those that inject content scripts into web pages.
Next.js shines in its intended use case - full-stack web applications. For Chrome extensions, stick with React and invest the saved bundle size in better user experiences.
Thank you,
Janith Sandaruwan.
linkedin
Top comments (2)
For most browser extensions, ANY framework is huge overkill.
You're not wrong! For context, here's when the overhead actually pays off:
Vanilla JS wins: Simple injectors, basic storage, single-function extensions
React justified: 3+ interconnected UI components, complex state, team development
I measured this - my vanilla bookmark extension was 12KB total. Perfect.
But my productivity dashboard extension hit 180 lines of just DOM manipulation code before I had any actual features. That's when React's 45KB overhead became worth it.
The real question isn't "framework or no framework" - it's "what's the complexity breaking point for your specific use case?"