← Tất cả bài viết

[ Blog ]

24 tháng 6, 2026

5 phút đọc

Cuộn mượt trong Next.js với GSAP & Lenis

Hướng dẫn thực chiến để có hiệu ứng cuộn mượt 60fps trong Next.js bằng Lenis và GSAP ScrollTrigger — cách cài đặt, animation theo scroll và các lỗi hiệu năng thường gặp.

Next.jsGSAPLenisAnimationHiệu năng
Cuộn mượt trong Next.js với GSAP & Lenis

Cuộn mượt (smooth scroll) là một trong những chi tiết phân biệt một template tầm thường với một website được chăm chút. Làm tốt, nó khiến animation theo scroll có cảm giác nặng và có chủ đích. Làm dở, nó "đánh nhau" với trình duyệt, rớt khung hình và phá vỡ khả năng tiếp cận. Bài này hướng dẫn cách thêm cuộn mượt cho Next.js đúng kiểu các site đoạt giải thưởng — dùng Lenis cho phần cuộn và GSAP ScrollTrigger cho animation — kèm cả những lỗi ít ai nhắc tới.

Chúng ta nhắm tới App Router (Next.js 14+), giữ 60fps và tôn trọng prefers-reduced-motion.

Vì sao chọn Lenis thay vì CSS hay thư viện scroll khác

Bạn có ba lựa chọn chính cho cuộn mượt, và chúng không ngang nhau.

Cách làmCảm giácHiệu năngKiểm soát
scroll-behavior: smooth (CSS)Chỉ mượt khi nhảy tới anchor, không mượt khi lăn chuộtTốtKhông
Thư viện "smooth scrollbar" nặngMượt nhưng hay giật, chiếm quyền layoutHên xuiTrung bình
LenisMượt theo quán tính tự nhiên cho cả chuột lẫn cảm ứngXuất sắcToàn quyền

Lenis được cộng đồng creative-dev ưa chuộng vì nó chuẩn hoá thao tác cuộn (lăn chuột, trackpad, cảm ứng) thành một giá trị đã được làm mượt, mà không giả lập thanh cuộn hay phá vỡ cuộn gốc của trình duyệt. Nó cũng cung cấp một giá trị cập nhật mỗi khung hình để bạn đưa thẳng vào GSAP — đúng thứ bạn cần cho animation theo scroll.

1. Cài đặt

npm install lenis gsap

2. Provider Lenis cho App Router

Cuộn mượt là việc của phía client, nên hãy bọc ứng dụng trong một client component nhỏ. Mẹo quan trọng để tối ưu hiệu năng: chạy Lenis và GSAP từ chung một vòng lặp requestAnimationFrame để chúng dùng chung một mốc thời gian và không "đánh nhau".

// app/lenis-provider.tsx
'use client';

import { ReactLenis, type LenisRef } from 'lenis/react';
import { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

export function LenisProvider({ children }: { children: React.ReactNode }) {
  const lenisRef = useRef<LenisRef>(null);

  useEffect(() => {
    function raf(time: number) {
      lenisRef.current?.lenis?.raf(time);
      ScrollTrigger.update();
    }
    gsap.ticker.add(raf);
    gsap.ticker.lagSmoothing(0);
    return () => gsap.ticker.remove(raf);
  }, []);

  return (
    <ReactLenis root options={{ autoRaf: false }} ref={lenisRef}>
      {children}
    </ReactLenis>
  );
}

Sau đó gắn nó vào root layout, bên trong <body>:

// app/layout.tsx
import { LenisProvider } from './lenis-provider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="vi">
      <body>
        <LenisProvider>{children}</LenisProvider>
      </body>
    </html>
  );
}

Vậy là xong — giờ bạn đã có cuộn mượt theo quán tính trên toàn site.

3. Animation theo scroll với ScrollTrigger

Vì Lenis cập nhật ScrollTrigger mỗi khung hình, animation của bạn đồng bộ hoàn hảo với scroll đã được làm mượt. Một section được ghim (pin) và scrub trông như sau:

'use client';
import { useRef } from 'react';
import { useGSAP } from '@gsap/react';
import { gsap } from 'gsap';

export function Reveal() {
  const ref = useRef<HTMLDivElement>(null);

  useGSAP(() => {
    gsap.to('.panel', {
      yPercent: -100,
      ease: 'none',
      scrollTrigger: {
        trigger: ref.current,
        start: 'top top',
        end: '+=150%',
        scrub: true,
        pin: true,
      },
    });
  }, { scope: ref });

  return <div ref={ref}>{/* các .panel con */}</div>;
}

scrub: true gắn tiến trình animation trực tiếp vào vị trí scroll, nên giá trị đã làm mượt của Lenis được đưa thẳng vào chuyển động. Đây cũng chính là kỹ thuật phía sau phần showcase được ghim trên hầu hết các trang case study.

4. Các lỗi hiệu năng cần tránh

Đây là chỗ phần lớn triển khai bị "vỡ". Ghi nhớ:

  • Chỉ animate transformopacity. Chúng chạy trên compositor và không kích hoạt layout. Animate top, width hay margin theo scroll sẽ làm rớt khung hình.
  • Một vòng lặp RAF duy nhất. Nếu Lenis tự chạy requestAnimationFrame GSAP chạy ticker riêng, bạn có hai vòng lặp và bị giật nhẹ. Provider ở trên tắt autoRaf của Lenis và để GSAP ticker điều khiển cả hai.
  • Gọi ScrollTrigger.refresh() sau khi layout thay đổi (font tải xong, đổi route, ảnh ổn định) để vị trí trigger luôn chính xác.
  • Lazy-load phần dưới màn hình. Cuộn mượt khiến trang dài trở nên hấp dẫn; giữ Core Web Vitals khoẻ mạnh bằng cách trì hoãn các component nặng.

5. Khả năng tiếp cận: tôn trọng reduced motion

Cuộn mượt và animation theo scroll lớn có thể gây chóng mặt. Luôn tôn trọng tuỳ chọn của người dùng — tắt easing và animation khi prefers-reduced-motion: reduce được bật:

const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// ví dụ: truyền `options={{ smoothWheel: !reduced }}` cho Lenis,
// và bọc animation ScrollTrigger trong `if (!reduced) { ... }`.

gsap.matchMedia() giúp việc này gọn gàng — đăng ký biến thể reduced-motion và GSAP tự đổi qua lại.

Câu hỏi thường gặp

Lenis có hại cho SEO hay Core Web Vitals không?

Không. Lenis làm mượt phần cuộn thị giác mà không chặn main thread nếu được cài với một vòng lặp RAF duy nhất. Nội dung vẫn là HTML render từ server, nên việc crawl và index không bị ảnh hưởng. Giữ animation ở transform/opacity thì CLS/INP vẫn khoẻ.

Có chạy với App Router và SSR của Next.js không?

Có. Provider là client component, nên nó hydrate ở client trong khi các trang vẫn được render từ server. Chỉ cần gắn một lần ở root layout.

Làm sao để cuộn tới một section?

Dùng lenis.scrollTo('#section') (hoặc một offset số). Lenis chặn cú nhảy và ease tới đích, nên điều hướng trong trang khớp với phần còn lại của trải nghiệm.


Cuộn mượt chỉ là một lượng code nhỏ nhưng tác động lớn tới cảm nhận chất lượng. Kết hợp Lenis với GSAP, giữ mọi thứ trên compositor, chạy từ một vòng lặp duy nhất và tôn trọng reduced motion — bạn sẽ có được cảm giác nặng, có chủ đích khiến người dùng ở lại.

Muốn tích hợp điều này vào sản phẩm của bạn? Xem cách tôi làm việc với các đội nhóm.