The Perks of Using Next.js - A 2019 Developer's Love Story
Why Next.js blew my mind in 2019 - Server-Side Rendering, automatic code splitting, and the magic of React without the configuration hell. A retrospective from the early adoption days when SSR felt like wizardry.
It’s August 2019, and I just discovered Next.js. After spending months wrestling with Webpack configurations, setting up Babel, configuring React Router, and trying to make Server-Side Rendering work with create-react-app (spoiler: it doesn’t), Next.js feels like magic. Let me tell you why this framework is about to change everything for React developers.
2019 Context
React Hooks just became stable (February 2019), create-react-app is the go-to starter, Gatsby is gaining traction for static sites, and most React apps are still client-side only. SSR is this mystical thing that only big companies like Netflix and Airbnb seem to have figured out. Enter Next.js 9.0…
The Problem: React Development in 2019
Let me paint you a picture of what React development looked like before Next.js became mainstream:
The Pain Points
- Configuration Hell: Webpack config files that looked like ancient spells
- SEO Nightmare: Search engines seeing empty
<div id="root"></div>
- Performance Issues: Massive JavaScript bundles killing mobile users
- Routing Complexity: React Router setup for every. single. project.
- SSR Complexity: Server-side rendering required PhD in Node.js
- Build Optimization: Code splitting? Good luck with that.
I remember spending entire weekends just trying to get a React app to render on the server. The amount of boilerplate, the complexity of hydration, the webpack configurations… it was enough to make you question your career choices.
// What a typical React setup looked like in 2019
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction ? '[name].[contenthash].js' : '[name].js',
publicPath: '/',
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { targets: 'defaults' }],
'@babel/preset-react',
'@babel/preset-typescript'
],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-syntax-dynamic-import'
]
}
}
},
{
test: /\.css$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader'
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
minify: isProduction
}),
...(isProduction ? [new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
})] : [])
],
optimization: {
minimize: isProduction,
minimizer: [
new TerserPlugin(),
new OptimizeCSSAssetsPlugin()
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
}
}
}
},
devServer: {
contentBase: path.join(__dirname, 'public'),
historyApiFallback: true,
hot: true,
port: 3000
}
};
};
And this was just the webpack config! You still needed separate configs for development, production, testing, and don’t get me started on trying to add SSR to this mess.
Enter Next.js: The Game Changer
Then I discovered Next.js, and my mind was blown. Here’s what the same project setup looks like:
npx create-next-app my-project
cd my-project
npm run dev
That’s it. Three commands, and you have:
- ✅ Server-Side Rendering
- ✅ Automatic code splitting
- ✅ File-based routing
- ✅ CSS and Sass support
- ✅ TypeScript support
- ✅ API routes
- ✅ Built-in optimization
The Magic of File-Based Routing
Coming from React Router, Next.js routing felt like cheating. Want a new page? Just create a file.
// pages/index.js - Homepage (automatically becomes /)
export default function Home() {
return <h1>Welcome to Next.js!</h1>;
}
// pages/about.js - About page (automatically becomes /about)
export default function About() {
return <h1>About Us</h1>;
}
// pages/blog/[slug].js - Dynamic routing
export default function BlogPost({ post }) {
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
export async function getServerSideProps({ params }) {
// This runs on the server for each request
const post = await fetchPost(params.slug);
return { props: { post } };
}
No routing configuration, no complex setup. The file system IS your router. This was revolutionary in 2019.
Server-Side Rendering Made Simple
Remember how complex SSR used to be? Here’s how you do it in Next.js:
// That's it. SSR happens automatically.
// But if you want to fetch data on the server:
export async function getServerSideProps(context) {
const { params, req, res, query } = context;
// Fetch data from API, database, etc.
const data = await fetch('https://api.example.com/data');
const result = await data.json();
return {
props: {
result
}
};
}
export default function MyPage({ result }) {
return (
<div>
<h1>Server-rendered data:</h1>
<pre>{JSON.stringify(result, null, 2)}</pre>
</div>
);
}
That’s it! No express server setup, no hydration complexity, no “universal” rendering configuration. Just… working SSR.
Automatic Code Splitting
One of the biggest performance wins was automatic code splitting. Every page gets its own bundle automatically.
// pages/heavy-page.js
import dynamic from 'next/dynamic';
// This component only loads when needed
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
loading: () => <p>Loading...</p>,
});
export default function HeavyPage() {
return (
<div>
<h1>This page loads fast</h1>
<HeavyComponent />
</div>
);
}
In 2019, this was mind-blowing. Webpack was capable of code splitting, but setting it up properly was an art form. Next.js made it automatic and intelligent.
API Routes: Full-Stack React
One of the most underrated features was API routes. You could build your backend right in your React app:
// pages/api/users.js
export default function handler(req, res) {
if (req.method === 'GET') {
// Handle GET request
res.status(200).json({ users: ['John', 'Jane'] });
} else if (req.method === 'POST') {
// Handle POST request
const { name } = req.body;
res.status(201).json({ message: `User ${name} created` });
}
}
// Frontend can call these APIs seamlessly:
const response = await fetch('/api/users');
const users = await response.json();
This was huge for rapid prototyping and small projects. No separate backend setup needed.
CSS and Styling Solutions
Styling in Next.js was refreshingly simple compared to the webpack CSS configuration nightmare:
// Built-in CSS Modules support
import styles from './Button.module.css';
export default function Button({ children }) {
return (
<button className={styles.button}>
{children}
</button>
);
}
/* Button.module.css */
.button {
background: blue;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
}
Or with styled-jsx (built-in):
export default function Button({ children }) {
return (
<>
<button>{children}</button>
<style jsx>{`
button {
background: blue;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
}
`}</style>
</>
);
}
Image Optimization (Next.js 10+)
While this came later, it’s worth mentioning how Next.js continued to solve real problems:
import Image from 'next/image';
export default function MyComponent() {
return (
<Image
src="/my-image.jpg"
alt="Description"
width={500}
height={300}
placeholder="blur" // Automatic blur placeholder
/>
);
}
Automatic optimization, lazy loading, and modern formats. In 2019, you had to set this up manually with complex webpack loaders.
The Developer Experience Revolution
What made Next.js special wasn’t just the features—it was the developer experience:
- Fast Refresh: Change code, see results instantly
- Error Overlay: Beautiful, helpful error messages
- Zero Config: Works out of the box for 90% of use cases
- Incremental Adoption: Could migrate page by page
- Great Documentation: Actually helpful, with examples
Real-World Impact: A Simple Blog
Here’s what a complete blog application looked like in Next.js 9 (2019):
// Project structure:
// pages/
// index.js // Homepage
// blog/
// index.js // Blog listing
// [slug].js // Individual blog posts
// api/
// posts.js // API to fetch posts
// posts/
// hello-world.md // Markdown posts
// next-js-rocks.md
// The entire app was ~500 lines of code
// Compare to equivalent Express + React setup: ~2000+ lines
This same application would have required:
- Express server setup
- Webpack configuration
- Babel configuration
- React Router setup
- SSR implementation
- Build optimization
- Development server setup
With Next.js, it was just pages and components.
Performance Wins That Mattered
In 2019, web performance was becoming critical. Next.js delivered:
Lighthouse Scores
- Before (CRA): ~60-70 average
- After (Next.js): ~90-95 average
Bundle Sizes
- Before: 500KB+ initial bundle
- After: <100KB initial bundle (automatic splitting)
Time to Interactive
- Before: 3-5 seconds on 3G
- After: 1-2 seconds on 3G
The Ecosystem Effect
Next.js didn’t just solve technical problems—it created an ecosystem:
- Vercel: Deployment made stupidly simple
- Next.js plugins: Community solutions for common needs
- Enterprise adoption: Companies started choosing Next.js for serious projects
- Learning curve: Much gentler introduction to React
Looking Back: What We Got Right
Writing this in 2024, it’s clear that Next.js made several prescient bets:
- File-based routing: Copied by many frameworks since
- API routes: Full-stack React became mainstream
- Automatic optimization: Zero-config became the standard
- SSR/SSG hybrid: Static generation gained huge traction
- Developer experience: DX became a competitive advantage
The Dark Side (Because Nothing’s Perfect)
Even in 2019, Next.js had some drawbacks:
The Challenges
- Learning curve: SSR concepts still complex for beginners
- Vendor lock-in: Vercel-specific features created dependency
- Bundle size: Next.js itself added framework overhead
- Magic: Too much “it just works” made debugging harder sometimes
Conclusion: Why Next.js Changed Everything
Next.js succeeded because it solved real problems that every React developer faced in 2019:
- Configuration fatigue: Made complex setups simple
- Performance by default: Optimization without thinking
- SEO and SSR: Made server rendering accessible
- Developer productivity: Focus on features, not tooling
The framework didn’t just add features—it removed complexity. In 2019, that was exactly what the React ecosystem needed.
Looking back, Next.js represented a maturation of the React ecosystem. It took the lessons learned from years of Webpack configurations, SSR struggles, and performance optimizations, and packaged them into a framework that “just worked.”
For developers in 2019, Next.js felt like magic. For developers today, it feels like common sense. That transformation from revolutionary to obvious is the mark of truly impactful technology.
The React ecosystem pre-Next.js was powerful but intimidating. Post-Next.js, it became accessible. That accessibility unleashed a wave of React adoption and innovation that we’re still seeing today.
If you’re reading this in 2024 and wondering what the fuss was about, consider yourself lucky. The tools you take for granted today were hard-won battles fought by frameworks like Next.js. The ecosystem was growing fast, and everything “just worked” with Next.js without complex configuration.
References
- Next.js 9 Release Notes - The version that changed everything
- React SSR Guide (2019) - What we had to do before Next.js
- Webpack Documentation - The complexity Next.js abstracted away
- Vercel Platform - The deployment platform built for Next.js