Frontend Development

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.

5 min read
nextjs react frontend ssr web-development javascript

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:

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:

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:

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

Bundle Sizes

Time to Interactive

The Ecosystem Effect

Next.js didn’t just solve technical problems—it created an ecosystem:

Looking Back: What We Got Right

Writing this in 2024, it’s clear that Next.js made several prescient bets:

  1. File-based routing: Copied by many frameworks since
  2. API routes: Full-stack React became mainstream
  3. Automatic optimization: Zero-config became the standard
  4. SSR/SSG hybrid: Static generation gained huge traction
  5. 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:

  1. Configuration fatigue: Made complex setups simple
  2. Performance by default: Optimization without thinking
  3. SEO and SSR: Made server rendering accessible
  4. 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