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:
After:
π§© 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:
After:
β 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
 


    
Top comments (0)