Pernah mengeklik link di web berbasis Next.js dan mendapati layar diam tanpa respon selama dua detik? Tidak ada indikator loading, kursor tetap biasa, dan layar seolah freeze. Pengguna yang tidak sabar biasanya bakal spam-klik tombol navigasi berkali-kali karena mengira aplikasi rusak, padahal server sedang memproses pre-render data di latar belakang.
Masalah UX klasik ini sering luput saat developer terlalu terpukau dengan kecepatan rendering Next.js.
Ilusi Aplikasi Macet di Balik Routing Next.js
Next.js sangat perkasa dalam urusan rendering data. Saat kamu memanfaatkan getStaticProps, perpindahan halaman terasa instan karena browser hanya mengambil data statis tanpa hard refresh. Begitu juga saat menggunakan getServerSideProps untuk mengambil data dinamis langsung dari server sebelum komponen dirender.
Masalahnya muncul saat koneksi pengguna sedang tidak ideal atau server butuh waktu merespons query database. Karena Next.js tidak melakukan reload browser tradisional yang memicu putaran loading bawaan tab browser, halaman akan diam mematung sampai proses data selesai ditarik. User dibiarkan menebak-nebak: apakah web ini sedang loading, atau memang crash?
Di sinilah peran penting progress bar di bagian atas layar. Indikator visual sederhana ini memberi umpan balik langsung ke pengguna bahwa aksi mereka terbaca, proses perpindahan rute sedang berjalan, dan estimasi beban tugas sedang dikerjakan.
Daripada memasang package wrapper instan seperti nextjs-progressbar tanpa tahu cara kerjanya, membuat komponen sendiri berbasis nprogress memberi kendali penuh terhadap manipulasi CSS, shallow routing, dan optimasi performa.
Membedah Arsitektur Komponen NProgress Custom
Komponen ini memanfaatkan event router bawaan Next.js (routeChangeStart, routeChangeComplete, dan routeChangeError) untuk memicu animasi progress bar secara otomatis.
Buat file baru di direktori komponen kamu, misalnya components/progressbar.tsx, lalu terapkan konfigurasi berikut:
tsimport Router from 'next/router'; import * as NProgress from 'nprogress'; import * as PropTypes from 'prop-types'; import * as React from 'react'; export interface NextNProgressProps { /** * The color of the bar. * @default "#29D" */ color?: string; /** * The start position of the bar. * @default 0.3 */ startPosition?: number; /** * The stop delay in milliseconds. * @default 200 */ stopDelayMs?: number; /** * The height of the bar. * @default 3 */ height?: number; /** * Whether to show the bar on shallow routes. * @default true */ showOnShallow?: boolean; /** * The other NProgress configuration options to pass to NProgress. * @default null */ options?: Partial<NProgress.NProgressOptions>; /** * The nonce attribute to use for the `style` tag. * @default undefined */ nonce?: string; /** * Use your custom CSS tag instead of the default one. * This is useful if you want to use a different style or minify the CSS. * @default (css) => <style nonce={nonce}>{css}</style> */ transformCSS?: (css: string) => JSX.Element; } const NextNProgress = ({ color = '#003865 linear-gradient(71.18deg, rgb(0, 34, 255)-27.32%, rgb(0, 34, 255)-16.39%, rgb(81, 121, 254)-7.38%, rgb(165, 237, 182) 30.59%, rgb(250, 232, 90) 46.06%, rgb(253, 172, 62) 62.61%, rgb(255, 92, 0) 75.82%);', startPosition = 0.3, stopDelayMs = 200, height = 3, showOnShallow = true, options, nonce, transformCSS = (css) => <style nonce={nonce}>{css}</style>, }: NextNProgressProps) => { let timer: NodeJS.Timeout | null = null; React.useEffect(() => { if (options) { NProgress.configure(options); } Router.events.on('routeChangeStart', routeChangeStart); Router.events.on('routeChangeComplete', routeChangeEnd); Router.events.on('routeChangeError', routeChangeError); return () => { Router.events.off('routeChangeStart', routeChangeStart); Router.events.off('routeChangeComplete', routeChangeEnd); Router.events.off('routeChangeError', routeChangeError); }; }, []); const routeChangeStart = ( _: string, { shallow, }: { shallow: boolean; } ) => { if (!shallow || showOnShallow) { NProgress.set(startPosition); NProgress.start(); } }; const routeChangeEnd = ( _: string, { shallow, }: { shallow: boolean; } ) => { if (!shallow || showOnShallow) { if (timer) clearTimeout(timer); timer = setTimeout(() => { NProgress.done(true); }, stopDelayMs); } }; const routeChangeError = ( _err: Error, _url: string, { shallow, }: { shallow: boolean; } ) => { if (!shallow || showOnShallow) { if (timer) clearTimeout(timer); timer = setTimeout(() => { NProgress.done(true); }, stopDelayMs); } }; return transformCSS(`#nprogress{pointer-events:none}#nprogress .bar{background:${color};position:fixed;z-index:9999;top:0;left:0;width:100%;height:${height}px}#nprogress .peg{display:block;position:absolute;right:0;width:100px;height:100%;box-shadow:0 0 10px ${color},0 0 5px ${color};opacity:1;-webkit-transform:rotate(3deg) translate(0,-4px);-ms-transform:rotate(3deg) translate(0,-4px);transform:rotate(3deg) translate(0,-4px)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}#nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:solid 2px transparent;border-top-color:${color};border-left-color:${color};border-radius:50%;-webkit-animation:nprogresss-spinner 400ms linear infinite;animation:nprogress-spinner 400ms linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .spinner,.nprogress-custom-parent #nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}`); }; NextNProgress.propTypes = { color: PropTypes.string, startPosition: PropTypes.number, stopDelayMs: PropTypes.number, height: PropTypes.number, showOnShallow: PropTypes.bool, options: PropTypes.object, nonce: PropTypes.string, transformCSS: PropTypes.func, }; export default React.memo(NextNProgress);
Konfigurasi Penting yang Perlu Diperhatikan
Beberapa prop di atas memiliki peran krusial terhadap performa dan tampilan:
| Properti | Tipe Data | Fungsi & Nilai Default |
|---|---|---|
color | String | Menentukan warna bar atau gradasi CSS kompleks. Default menggunakan multi-color gradient. |
startPosition | Number | Titik awal animasi bar saat route mulai berubah (Default: 0.3). |
stopDelayMs | Number | Jeda waktu sebelum bar menghilang sepenuhnya saat route selesai (Default: 200ms). |
showOnShallow | Boolean | Menentukan apakah bar tetap aktif pada perubahan query params / shallow routing (Default: true). |
transformCSS | Function | Menginjeksi custom inline style tag secara dinamis tanpa file CSS eksternal terpisah. |
Menerapkan Komponen ke Global Layout
Agar progress bar terpanggil di setiap transisi halaman tanpa perlu dipasang satu per satu, pasang komponen ini pada file wrapper tata letak utama aplikasi, misalnya di pages/layout/index.tsx atau file layout utama codebase kamu.
jsimport Header from "../../components/header" import Footer from "../../components/footer" // import custom progress bar component import NextNProgress from '../../components/nprogress'; const Layout = ({ categories, children }: Props) => { return ( <> <div className="relative h-screen bg-gradient-to-b-white dark:bg-gradient-to-b lg:h-[140vh]"> <NextNProgress options={{ showSpinner: false }} /> <Header categories={categories} /> <main>{children}</main> <Footer /> </div> </> ) } export default Layout;
Dengan mematikan showSpinner: false, tampilan bar menjadi lebih clean di bagian atas viewport tanpa icon loading bulat yang mengganggu elemen header.
Saat rute berpindah, animasi garis langsung berjalan halus memotong layar, memberi kepastian visual kepada pengguna bahwa permintaan mereka sedang dieksekusi secara instan. Hasil visualnya memberikan indikasi navigasi yang jelas dan responsif:
![]() |
|---|
| Ilustrasi Progressbar saat navigasi berjalan |
Detail visual kecil seperti ini sering diremehkan, namun dampaknya luar biasa dalam mengubah persepsi pengguna terhadap kecepatan aplikasi web kamu.

