DEV Community

Zineb Esso
Zineb Esso

Posted on • Edited on

How I Optimized a Next.js +Refine + Ant Design Project for Lightning-Fast Performance πŸš€

When working with frameworks like Refine, Next.js, and Ant Design, it's easy to build powerful apps β€” but making them fast and optimized takes some extra steps.

In this post, I'll show you how I took a standard Refine + Next.js project and customized it for better performance, faster loading times, and smoother UX.

(And yes, I'll share Core Web Vitals scores before and after the optimizations! πŸ“ˆ)


πŸ”₯ Quick Demo

Here's a quick demo showing the performance difference after optimization:

Before:

Before Optimization

After:

After Optimization

🧩 Stack Overview

What is Refine?

Refine is a headless React framework focused on building internal tools, admin panels, and dashboards β€” making CRUD operations much easier.

What is Ant Design (antd)?

Ant Design is an enterprise-class UI library offering a large collection of well-designed React components, perfect for clean, consistent UIs.

βš™οΈ Step 1: Customizing next.config.mjs

The first big move was tuning the Next.js configuration to make imports smarter, bundles smaller, and builds faster.

Here's the updated next.config.mjs:

/** @type {import('next').NextConfig} */
const nextConfig = {
  transpilePackages: [
    "@refinedev/core",
    "@refinedev/devtools",
    "@refinedev/nextjs-router",
    "@refinedev/kbar",
    "@refinedev/nestjsx-crud",
    "@refinedev/antd",
    "@ant-design/icons",
    "antd",
  ],
  experimental: {
    optimizePackageImports: [
      "@refinedev/core",
      "@refinedev/devtools",
      "@refinedev/nextjs-router",
      "@refinedev/kbar",
      "@refinedev/nestjsx-crud",
      "@refinedev/antd",
      "@ant-design/icons",
      "antd",
    ],
  },
  swcMinify: true,
  modularizeImports: {
    antd: {
      transform: "antd/es/{{member}}",
      preventFullImport: true,
    },
    "@ant-design/icons": {
      transform: "@ant-design/icons/es/icons/{{member}}",
      preventFullImport: true,
    },
  },
  compiler: {
    reactRemoveProperties: true,
    removeConsole: { exclude: ["error", "warn"] },
  },
  output: "standalone",
};

export default nextConfig;
Enter fullscreen mode Exit fullscreen mode

🧠 Key Config Changes:

  • Transpile external packages for better compatibility
  • Optimize package imports for tree-shaking
  • Modularize imports to avoid loading full libraries
  • Remove console logs and non-essential props from production
  • Standalone output for lighter deployments (Docker, serverless)

βœ… Result: Faster builds, smaller bundles, and better runtime performance.

πŸš€ Step 2: Adding a Global loading.tsx Component

We don't want users staring at blank screens, right?

I added a global loading indicator:

// app/loading.tsx
const Loading = () => (
  <div className="flex items-center justify-center h-screen text-lg">
    Loading...
  </div>
);

export default Loading;
Enter fullscreen mode Exit fullscreen mode

Why This Matters:

  • πŸ“± Gives instant feedback while components load
  • ⚑ Improves "perceived performance" (even if load time is the same)
  • 🎯 Reduces Largest Contentful Paint (LCP) and layout shifts
  • πŸ“ˆ Improves SEO and Core Web Vitals

βœ… Result: Faster-feeling app + better UX from the user's perspective.

⚑ Step 3: Dynamic Imports with next/dynamic

Instead of using React.lazy, Next.js offers dynamic() for better optimization.

Here’s how I used it:

import dynamic from "next/dynamic";
import { Suspense } from "react";
import LoadingSpinner from "@context/loadingSpinner";

const BlogPostCreateComponent = dynamic(
  () => import("@components/blog/create"),
  {
    ssr: false,
    loading: () => <LoadingSpinner />,
  }
);

export default function BlogPostCreate() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <BlogPostCreateComponent />
    </Suspense>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why Use dynamic() Instead of lazy()?

  • 🏎️ Built-in to Next.js and integrates perfectly
  • πŸ”₯ Control over SSR (disable server-side rendering if needed)
  • πŸ“¦ Automatic code splitting for lighter pages
  • 🎑 Custom loading states (better than default browser loading)

βœ… Result: Less initial JavaScript, quicker interaction, and smoother page transitions.

🎨 Step 4: Tailwind CSS Optimization with JIT Mode

Tailwind can get bloated if not handled properly.

That's why I enabled Just-in-Time (JIT) mode in tailwind.config.js:

/** @type {import('tailwindcss').Config} */
export default {
  mode: 'jit',
  content: [
    './src/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: { extend: {} },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • ⚑ Faster build times
  • 🧹 Smaller final CSS bundle
  • 🎯 Only generates classes you actually use

βœ… Result: Clean, efficient, production-ready CSS.

πŸ“¦ Running the Project

pnpm install     # Install dependencies
pnpm dev         # Start development server
pnpm build       # Create a production build
pnpm start       # Launch production server
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Pro Tip: Production optimizations (like tree-shaking and minification) are only fully applied after pnpm build.

πŸ“ˆ Performance Comparison: Before vs After

Metric Before After
Bundle Size Huge Reduced
Initial Load Time Slower Faster
Console Noise Lots Clean
User Experience Choppy Smooth
Core Web Vitals 🚫 Poor βœ… Improved

πŸ“Š Core Web Vitals Improvement

Before:

Before Optimization

After:

After Optimization

βœ… Noticeable improvement in LCP, FCP, CLS, and TTFB scores!

βœ… Conclusion

By optimizing the Next.js config, introducing better loading strategies, dynamically importing components, and cleaning up the Tailwind setup, I transformed a good app into a great, fast, and scalable one.

These changes led to:

  • πŸš€ Faster load times
  • πŸ“ˆ Better SEO and Web Vitals
  • 🧩 Easier long-term maintenance

πŸ”— Check the Full Source Code

GitHub Repo (Link)

Top comments (0)