DEV Community

Cover image for Building Chrome Extensions: React vs Next.js - My Experience
J-Sandaruwan
J-Sandaruwan

Posted on

Building Chrome Extensions: React vs Next.js - My Experience

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
Enter fullscreen mode Exit fullscreen mode

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: '.' }
      ]
    })
  ]
};

Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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 />);

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

}
};

module.exports = nextConfig;




### Project Structure Issues



Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
jonrandy profile image
Jon Randy 🎖️

For most browser extensions, ANY framework is huge overkill.

Collapse
 
jsandaruwan profile image
J-Sandaruwan

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?"