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;

🧠 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;

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

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: [],
};

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

👉 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)