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)