0
0
Lập trình
Admin Team
Admin Teamtechmely

Làm Chủ Kỹ Thuật Code Splitting Trong Next.js App Router

Đăng vào 7 tháng trước

• 6 phút đọc

Giới Thiệu

Chào các lập trình viên! Sau khi dẫn dắt nhiều dự án Next.js và chứng kiến sự khó khăn của các lập trình viên mới trong việc xử lý kích thước bundle, tôi nghĩ đã đến lúc chia sẻ một số chiến lược đã thử nghiệm cho việc code splitting trong App Router.

Thực Trạng: Tại Sao Người Dùng Của Bạn Bỏ Cuộc?

Hãy tưởng tượng: Bạn đã xây dựng một nền tảng thương mại điện tử tuyệt vời với tất cả các tính năng mà bạn có thể tưởng tượng. Biểu đồ, bản đồ, trình chỉnh sửa văn bản phong phú, chức năng xuất PDF - tất cả đều có mặt. Nhưng người dùng của bạn lại nhấn nút quay lại nhanh hơn cả khi bạn nói "dynamic import."

Thủ phạm? Chính là gói 2MB ban đầu bao gồm toàn bộ tính năng, mặc dù 80% người dùng không bao giờ cần đến bảng điều khiển quản trị hay tạo một PDF nào.

App Router: Thay Đổi Cuộc Chơi (Nhưng Không Phải Phép Màu)

Với Next.js 13+ App Router, các thành phần server của React tự động được code-split theo mặc định. Đây là tin tuyệt vời, nhưng có một điều cần lưu ý - các thành phần client vẫn cần chú ý của chúng ta. Tin tôi đi, đó là nơi phép thuật tối ưu hóa thực sự diễn ra.

Tình Huống Thực Tế: Xây Dựng Một Dashboard SaaS

Hãy để tôi dẫn bạn qua một dự án thực tế, một dashboard phân tích SaaS. Có nhiều tính năng nặng nề:

  • Một dashboard trực quan dữ liệu phức tạp (Chart.js + D3)
  • Một trình chỉnh sửa văn bản phong phú cho báo cáo (Monaco Editor)
  • Chức năng xuất PDF (jsPDF)
  • Widget hỗ trợ chat thời gian thực

Cách Thông Minh: Phân Tách Thành Phần Chiến Lược

Dưới đây là cách chúng tôi giải quyết dashboard với dynamic imports:

javascript Copy
'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'

// Lazy load heavy components
const AnalyticsDashboard = dynamic(() => import('./AnalyticsDashboard'), {
  loading: () => <DashboardSkeleton />, // Hiển thị khi đang tải
  ssr: false // Không render trên server
})

const ReportEditor = dynamic(() => import('./ReportEditor'), {
  loading: () => <EditorSkeleton />
})

const ChatWidget = dynamic(() => import('./ChatWidget'), {
  ssr: false // Widget chỉ cho trình duyệt với các phụ thuộc window/localStorage
})

export default function DashboardPage() {
  const [activeTab, setActiveTab] = useState('overview')
  const [showChat, setShowChat] = useState(false)

  return (
    <div className="dashboard">
      <nav>
        <button onClick={() => setActiveTab('overview')}>Tổng Quan</button>
        <button onClick={() => setActiveTab('analytics')}>Phân Tích</button>
        <button onClick={() => setActiveTab('reports')}>Báo Cáo</button>
      </nav>

      {activeTab === 'analytics' && <AnalyticsDashboard />}
      {activeTab === 'reports' && <ReportEditor />}
      {showChat && <ChatWidget onClose={() => setShowChat(false)} />}
    </div>
  )
}

Kết Quả: Kích thước bundle ban đầu giảm từ 1.8MB xuống còn 350KB. Người dùng ở tab tổng quan có trải nghiệm mượt mà hơn.

Mẹo Chuyên Nghiệp: Tải Thư Viện Bên Ngoài Thông Minh

Đây là một mẫu mà tôi sử dụng cho các phụ thuộc bên ngoài nặng nề. Thay vì nhập jsPDF ở cấp cao nhất, chúng tôi tải nó theo yêu cầu:

javascript Copy
'use client'
import { useState, useRef } from 'react'

export default function InvoiceGenerator() {
  const [isGenerating, setIsGenerating] = useState(false)
  const jsPdfRef = useRef(null)

  const generatePDF = async (invoiceData) => {
    setIsGenerating(true)

    // Tải jsPDF chỉ khi thực sự cần
    if (!jsPdfRef.current) {
      const jsPdfModule = await import('jspdf')
      jsPdfRef.current = jsPdfModule.default
    }

    const pdf = new jsPdfRef.current()
    // ... logic tạo PDF

    setIsGenerating(false)
  }

  const preloadPDF = async () => {
    // Tải trước khi người dùng di chuột qua nút
    if (!jsPdfRef.current) {
      const jsPdfModule = await import('jspdf')
      jsPdfRef.current = jsPdfModule.default
    }
  }

  return (
    <button 
      onMouseEnter={preloadPDF}
      onClick={() => generatePDF(invoiceData)}
      disabled={isGenerating}
    >
      {isGenerating ? 'Đang Tạo...' : 'Tải Xuống PDF'}
    </button>
  )
}

Mẫu này mang đến cho chúng tôi lợi ích của cả hai thế giới: kích thước gói ban đầu nhỏ và trải nghiệm người dùng phản hồi qua việc tải trước.

Khía Cạnh Server Component

Đây là thứ đã làm nhiều lập trình viên nhầm lẫn ban đầu - bạn có thể nhập động các thành phần server:

javascript Copy
import dynamic from 'next/dynamic'

const HeavyServerComponent = dynamic(() => import('./HeavyServerComponent'))

export default function ServerPage() {
  return (
    <div>
      <h1>Trang Của Tôi</h1>
      <HeavyServerComponent />
    </div>
  )
}

Nhưng đây là điều quan trọng: điều này không tối ưu hóa việc tải phía server. Điều này cho phép lazy loading bất kỳ thành phần client nào lồng ghép bên trong thành phần server đó. Đây là một sự phân biệt tinh tế nhưng quan trọng.

Khi Nào KHÔNG Nên Sử Dụng Dynamic Imports

Học hỏi từ những sai lầm (vâng, tôi cũng đã mắc phải):

Đừng nhập động:

  • Nội dung ở trên-fold (kẻ giết LCP)
  • Các thành phần tiện ích nhỏ (<5KB)
  • Các yếu tố điều hướng quan trọng
  • Các thành phần cần thiết cho SEO

Nên nhập động:

  • Các hộp thoại modal và overlay
  • Các thành phần nặng nề theo tính năng
  • Các widget bên thứ ba
  • Các bảng điều khiển quản trị và các tính năng ít sử dụng
  • Các thành phần cụ thể cho thiết bị

Mẫu Nâng Cao: Tải Tính Năng Điều Kiện

Đối với SaaS của chúng tôi, chúng tôi đã triển khai tải dựa trên cờ tính năng:

javascript Copy
'use client'
import { useFeatureFlag } from '@/hooks/useFeatureFlag'
import dynamic from 'next/dynamic'

const AdvancedAnalytics = dynamic(() => import('./AdvancedAnalytics'))
const BasicAnalytics = dynamic(() => import('./BasicAnalytics'))

export default function AnalyticsWrapper() {
  const hasAdvancedFeatures = useFeatureFlag('advanced-analytics')

  return hasAdvancedFeatures ? <AdvancedAnalytics /> : <BasicAnalytics />
}

Điều này cho phép chúng tôi giao hàng với các kích thước bundle khác nhau dựa trên các cấp độ đăng ký của người dùng. Người dùng cao cấp có các thư viện biểu đồ nặng; người dùng cơ bản có phiên bản nhẹ hơn.

Đo Lường Thành Công: Những Con Số Quan Trọng

Sau khi triển khai các mẫu này trên dashboard của chúng tôi:

  • Kích thước bundle ban đầu: 1.8MB → 350KB (-81%)
  • Thời gian tương tác: 4.2s → 1.8s (-57%)
  • Thời gian cho Contentful Paint đầu tiên: 2.1s → 0.9s (-57%)
  • Tỷ lệ thoát: 23% → 8% (-65%)

Lợi Ích Trải Nghiệm Lập Trình

Một lợi ích không ngờ: các bản dựng phát triển của chúng tôi trở nên nhanh chóng hơn nhiều. Các lần tải lại nóng giảm từ 3-4 giây xuống dưới 1 giây vì chúng tôi không biên dịch các khối lớn không cần thiết.

Kết Luận

Code splitting trong App Router không chỉ là về hiệu suất - mà còn là xây dựng các ứng dụng bền vững, có thể mở rộng. Bắt đầu với các tính năng nặng (biểu đồ, trình chỉnh sửa, PDF), triển khai tải trước thông minh và luôn đo lường tác động.

Hãy nhớ: người dùng của bạn không quan tâm đến các tính năng đẹp mắt của bạn nếu họ không thể tải ứng dụng của bạn. Hãy làm cho trải nghiệm ban đầu nhanh chóng, sau đó nâng cấp dần với dynamic imports.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào