Kỹ thuật tối ưu chạy được trên React, Vue, Angular và Svelte

Lighthouse chấm 38 điểm, bundle 2.4MB, LCP 6.2 giây. Đổi sang framework khác không giải quyết được, vì vấn đề nằm ở cách dùng, không phải framework. Tree shaking thiếu config, lazy loading chưa áp dụng, render không cần thiết chạy liên tục. Chín kỹ thuật dưới đây hoạt động trên React, Vue, Angular và Svelte vì chúng giải quyết vấn đề ở tầng runtime và build, không phải tầng framework.

Mục lục

  1. Tối ưu hóa bundle size
  2. Code-splitting và lazy-loading
  3. Tối ưu hóa rendering
  4. Quản lý state hiệu quả
  5. Tối ưu hóa hình ảnh và media
  6. Caching và memoization
  7. Server-side rendering và Static site generation
  8. Tối ưu hóa Web Vitals
  9. Theo dõi và phân tích hiệu suất
  10. Chín kỹ thuật tối ưu áp dụng được cho mọi framework

Tối ưu hóa bundle size

Kích thước bundle là yếu tố quan trọng ảnh hưởng đến thời gian tải trang. Dưới đây là các kỹ thuật giúp giảm kích thước bundle cho mọi framework:

1. Tree shaking

Tree shaking là quá trình loại bỏ code không sử dụng khỏi bundle cuối cùng. Hầu hết các bundler hiện đại (webpack, Rollup, esbuild, Vite) đều hỗ trợ tree shaking.

// Thay vì import toàn bộ thư viện
import _ from "lodash";

// Chỉ import những gì bạn cần
import map from "lodash/map";
import filter from "lodash/filter";

2. Phân tích bundle

Sử dụng các công cụ như webpack-bundle-analyzer, rollup-plugin-visualizer hoặc vite-bundle-visualizer để phân tích kích thước bundle và xác định các dependency lớn.

# Cài đặt webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer

# Hoặc với Vite
npm install --save-dev vite-bundle-visualizer

3. Sử dụng các thư viện nhỏ gọn

Ưu tiên các thư viện nhỏ gọn hoặc các phiên bản nhẹ của thư viện phổ biến.

// Thay vì moment.js (nặng)
import moment from "moment";

// Sử dụng date-fns (nhẹ hơn nhiều)
import { format, addDays } from "date-fns";

// Hoặc sử dụng Day.js (API tương tự moment nhưng nhẹ hơn)
import dayjs from "dayjs";

4. Compression

Đảm bảo server của bạn hỗ trợ nén Gzip hoặc Brotli cho tất cả các tài nguyên tĩnh.

# Cấu hình Nginx cho Gzip
gzip on;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;

Code-splitting và lazy-loading

Chia nhỏ ứng dụng thành các chunk nhỏ hơn và chỉ tải khi cần thiết giúp cải thiện thời gian tải trang ban đầu.

1. Dynamic import

Tất cả các framework hiện đại đều hỗ trợ dynamic import để lazy-load các component và module.

// React
const LazyComponent = React.lazy(() => import("./LazyComponent"));

// Vue 3
const LazyComponent = () => import("./LazyComponent.vue");

// Angular
const routes = [
  {
    path: "lazy",
    loadChildren: () => import("./lazy/lazy.module").then((m) => m.LazyModule),
  },
];

// Svelte với SvelteKit
const LazyComponent = () => import("./LazyComponent.svelte");

2. Route-based code splitting

Chia code theo route là cách hiệu quả để giảm kích thước bundle ban đầu.

// React Router
import { lazy } from "react";

const Home = lazy(() => import("./pages/Home"));
const About = lazy(() => import("./pages/About"));
const Contact = lazy(() => import("./pages/Contact"));

// Vue Router
const routes = [
  {
    path: "/",
    component: () => import("./pages/Home.vue"),
  },
  {
    path: "/about",
    component: () => import("./pages/About.vue"),
  },
];

3. Component-level code splitting

Lazy-load các component lớn hoặc ít sử dụng.

// Lazy-load một modal dialog chỉ khi cần
const Modal = lazy(() => import("./components/Modal"));

function App() {
  const [showModal, setShowModal] = useState(false);

  return (
    <div>
      <button onClick={() => setShowModal(true)}>Open Modal</button>
      {showModal && (
        <Suspense fallback={<div>Loading...</div>}>
          <Modal onClose={() => setShowModal(false)} />
        </Suspense>
      )}
    </div>
  );
}

Tối ưu hóa rendering

Tối ưu hóa quá trình rendering giúp giảm thiểu thời gian xử lý và cải thiện độ mượt của UI.

1. Virtualization

Sử dụng virtualization cho danh sách dài hoặc bảng có nhiều dữ liệu.

// React với react-window
import { FixedSizeList } from 'react-window';

function VirtualList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );

  return (
    <FixedSizeList
      height={500}
      width="100%"
      itemCount={items.length}
      itemSize={35}
    >
      {Row}
    </FixedSizeList>
  );
}

// Vue với vue-virtual-scroller
<template>
  <RecycleScroller
    class="scroller"
    :items="items"
    :item-size="32"
    key-field="id"
  >
    <template v-slot="{ item }">
      <div class="user">
        {{ item.name }}
      </div>
    </template>
  </RecycleScroller>
</template>

2. Tránh reflow và repaint không cần thiết

Nhóm các thay đổi DOM và sử dụng các thuộc tính CSS hiệu quả.

// Kém hiệu quả - gây nhiều reflow
function animateBadly() {
  const element = document.getElementById("my-element");
  for (let i = 0; i < 100; i++) {
    element.style.left = i + "px"; // Mỗi thay đổi gây reflow
  }
}

// Tốt hơn - sử dụng CSS transforms
function animateWell() {
  const element = document.getElementById("my-element");
  element.style.transition = "transform 1s";
  element.style.transform = "translateX(100px)";
}

3. Sử dụng CSS containment

Giúp trình duyệt tối ưu hóa rendering bằng cách cô lập các phần của trang.

.container {
  contain: content;
}

.card {
  contain: layout style paint;
}

4. Content-visibility và contain-intrinsic-size

Sử dụng content-visibility: auto để trì hoãn rendering các phần tử ngoài viewport.

.section {
  content-visibility: auto;
  contain-intrinsic-size: 500px; /* Ước tính kích thước */
}

Quản lý state hiệu quả

Quản lý state kém hiệu quả có thể dẫn đến re-render không cần thiết và làm giảm hiệu suất.

1. Phân chia state

Chia nhỏ state thành các phần độc lập để tránh re-render toàn bộ ứng dụng.

// React với Context API
const UserContext = createContext();
const ThemeContext = createContext();

function App() {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState("light");

  return (
    <UserContext.Provider value={{ user, setUser }}>
      <ThemeContext.Provider value={{ theme, setTheme }}>
        <MainContent />
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

// Vue với Pinia
// userStore.js
export const useUserStore = defineStore("user", {
  state: () => ({ user: null }),
  actions: {
    setUser(user) {
      this.user = user;
    },
  },
});

// themeStore.js
export const useThemeStore = defineStore("theme", {
  state: () => ({ theme: "light" }),
  actions: {
    setTheme(theme) {
      this.theme = theme;
    },
  },
});

2. Immutability

Sử dụng cấu trúc dữ liệu bất biến để dễ dàng phát hiện thay đổi và tối ưu hóa rendering.

// Kém hiệu quả - thay đổi trực tiếp
function addTodo(todos, newTodo) {
  todos.push(newTodo);
  return todos;
}

// Tốt hơn - sử dụng immutability
function addTodo(todos, newTodo) {
  return [...todos, newTodo];
}

// Hoặc sử dụng thư viện như Immer
import produce from "immer";

function addTodo(todos, newTodo) {
  return produce(todos, (draft) => {
    draft.push(newTodo);
  });
}

3. Memoization của component

Sử dụng các kỹ thuật memoization để tránh re-render không cần thiết.

// React với memo, useMemo và useCallback
const MemoizedComponent = React.memo(function MyComponent(props) {
  return <div>{props.name}</div>;
});

function ParentComponent() {
  const [count, setCount] = useState(0);

  // Chỉ tạo lại hàm khi dependencies thay đổi
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []);

  // Chỉ tính toán lại khi dependencies thay đổi
  const expensiveValue = useMemo(() => {
    return computeExpensiveValue(count);
  }, [count]);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <MemoizedComponent name="John" onClick={handleClick} />
    </div>
  );
}

// Vue với computed và v-memo
<template>
  <div>
    <button @click="count++">Increment</button>
    <div v-memo="[name]">{{ name }}</div>
    <div>{{ expensiveValue }}</div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

const count = ref(0);
const name = ref('John');

const expensiveValue = computed(() => {
  return computeExpensiveValue(count.value);
});
</script>

Tối ưu hóa hình ảnh và media

Hình ảnh và media thường chiếm phần lớn kích thước trang web và có thể ảnh hưởng đáng kể đến hiệu suất.

1. Lazy-loading hình ảnh

Sử dụng thuộc tính loading="lazy" hoặc Intersection Observer API.

<!-- Native lazy-loading -->
<img
  src="image.jpg"
  loading="lazy"
  alt="Description"
  width="800"
  height="600"
/>

<!-- Với Intersection Observer -->
<script>
  document.addEventListener("DOMContentLoaded", function () {
    const images = document.querySelectorAll(".lazy-image");

    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;
          observer.unobserve(img);
        }
      });
    });

    images.forEach((img) => {
      observer.observe(img);
    });
  });
</script>

2. Responsive images

Sử dụng srcsetsizes để cung cấp hình ảnh phù hợp với kích thước màn hình.

<img
  src="small.jpg"
  srcset="small.jpg 500w, medium.jpg 1000w, large.jpg 1500w"
  sizes="(max-width: 600px) 500px, (max-width: 1200px) 1000px, 1500px"
  alt="Responsive image"
  width="800"
  height="600"
/>

3. Modern image formats

Sử dụng các định dạng hiện đại như WebP và AVIF với fallback.

<picture>
  <source type="image/avif" srcset="image.avif" />
  <source type="image/webp" srcset="image.webp" />
  <img src="image.jpg" alt="Description" width="800" height="600" />
</picture>

4. Image CDN và optimization services

Sử dụng các dịch vụ như Cloudinary, Imgix hoặc Cloudflare Images để tự động tối ưu hóa hình ảnh.

<!-- Cloudinary example -->
<img
  src="https://res.cloudinary.com/demo/image/upload/w_800,f_auto,q_auto/sample.jpg"
  alt="Optimized with Cloudinary"
  width="800"
  height="600"
/>

Caching và memoization

Caching giúp tránh tính toán lại các giá trị hoặc tải lại dữ liệu không thay đổi.

1. Memoization functions

Tạo các hàm memoized để cache kết quả của các tính toán phức tạp.

// Memoization đơn giản
function memoize(fn) {
  const cache = new Map();
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// Sử dụng
const expensiveCalculation = memoize((a, b) => {
  console.log("Calculating...");
  return a * b;
});

console.log(expensiveCalculation(4, 2)); // Logs: Calculating... 8
console.log(expensiveCalculation(4, 2)); // Logs: 8 (từ cache)

2. HTTP Caching

Sử dụng HTTP caching headers để tối ưu hóa việc tải lại tài nguyên.

// Cấu hình Express.js
app.use(
  express.static("public", {
    etag: true,
    lastModified: true,
    maxAge: "1d", // Cache trong 1 ngày
    setHeaders: (res, path) => {
      if (path.endsWith(".html")) {
        // Không cache file HTML
        res.setHeader("Cache-Control", "no-cache");
      } else if (path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg)$/)) {
        // Cache tài nguyên tĩnh
        res.setHeader("Cache-Control", "public, max-age=31536000"); // 1 năm
      }
    },
  })
);

3. Service Worker caching

Sử dụng Service Workers để cache tài nguyên và cung cấp trải nghiệm offline.

// service-worker.js
const CACHE_NAME = "my-site-cache-v1";
const urlsToCache = [
  "/",
  "/styles/main.css",
  "/scripts/main.js",
  "/images/logo.png",
];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(urlsToCache);
    })
  );
});

self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // Cache hit - return response
      if (response) {
        return response;
      }
      return fetch(event.request);
    })
  );
});

4. Data caching với SWR hoặc React Query

Sử dụng thư viện caching data để tối ưu hóa các request API.

// React với SWR
import useSWR from "swr";

function Profile() {
  const { data, error } = useSWR("/api/user", fetcher, {
    revalidateOnFocus: false,
    dedupingInterval: 60000, // 1 phút
  });

  if (error) return <div>Failed to load</div>;
  if (!data) return <div>Loading...</div>;
  return <div>Hello {data.name}!</div>;
}
<!-- Vue với TanStack Query (v5, 2023+), không còn package `vue-query` -->
<script setup>
import { useQuery } from "@tanstack/vue-query";

const { data, isPending, error } = useQuery({
  queryKey: ["users"],
  queryFn: fetchUsers,
  staleTime: 60_000, // 1 phút
  gcTime: 15 * 60_000, // 15 phút (v5 đổi tên `cacheTime` → `gcTime`)
});
</script>

Breaking change 2023+: TanStack Query v5 đã đổi cacheTimegcTime, isLoadingisPending cho trạng thái fetch ban đầu. Dùng phiên bản cũ thì giữ API cũ; upgrade thì đổi theo.

Server-side rendering và Static site generation

SSR và SSG giúp cải thiện thời gian tải trang đầu tiên và SEO.

1. Framework-agnostic approaches

Các nguyên tắc SSR và SSG có thể áp dụng cho mọi framework.

// Ví dụ đơn giản về SSR với Express và React
import express from "express";
import React from "react";
import { renderToString } from "react-dom/server";
import App from "./App";

const app = express();

app.get("/", (req, res) => {
  const html = renderToString(<App />);
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <title>My SSR App</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script src="/bundle.js"></script>
      </body>
    </html>
  `);
});

app.listen(3000);

2. Framework-specific solutions

Mỗi framework có giải pháp riêng cho SSR và SSG.

# Next.js (React)
npx create-next-app my-next-app

# Nuxt.js (Vue)
npx create-nuxt-app my-nuxt-app

# SvelteKit (Svelte)
npm create svelte@latest my-sveltekit-app

# Angular Universal
ng add @nguniversal/express-engine

3. Incremental Static Regeneration (ISR)

ISR kết hợp lợi ích của SSG và SSR, cho phép tạo lại trang tĩnh theo thời gian.

// Next.js với ISR
export async function getStaticProps() {
  const res = await fetch("https://api.example.com/data");
  const data = await res.json();

  return {
    props: { data },
    revalidate: 60, // Tái tạo trang sau 60 giây
  };
}

export async function getStaticPaths() {
  return {
    paths: [{ params: { id: "1" } }, { params: { id: "2" } }],
    fallback: "blocking", // Tạo trang mới theo yêu cầu
  };
}

Tối ưu hóa Web Vitals

Tối ưu hóa Core Web Vitals giúp cải thiện SEO và trải nghiệm người dùng.

1. Largest Contentful Paint (LCP)

Tối ưu hóa thời gian hiển thị phần tử lớn nhất trong viewport.

<!-- Preload hero image -->
<link rel="preload" as="image" href="/hero.jpg" fetchpriority="high" />

<!-- Inline critical CSS -->
<style>
  /* Critical CSS cho above-the-fold content */
</style>

<!-- Defer non-critical CSS -->
<link
  rel="stylesheet"
  href="/styles.css"
  media="print"
  onload="this.media='all'"
/>

2. Interaction to Next Paint (INP), tối ưu độ trễ tương tác

INP là chỉ số tương tác trong Core Web Vitals (thay FID từ 2024). INP đo lâu nhất toàn bộ chu kỳ từ user input → next paint (xử lý handler + render + presentation). Cải thiện:

// 1. Dynamic import để giảm JS phải parse trước tương tác
const heavyComponent = () => import("./HeavyComponent");

// 2. Web Worker cho tác vụ nặng, không chiếm main thread
const worker = new Worker(new URL("./worker.js", import.meta.url), {
  type: "module",
});
worker.postMessage({ data: complexData });
worker.onmessage = (e) => updateUI(e.data);

// 3. Tối ưu scroll handler với passive + rAF
let scrollTimeout;
function optimizedScrollHandler() {
  if (scrollTimeout) cancelAnimationFrame(scrollTimeout);
  scrollTimeout = requestAnimationFrame(() => {
    /* xử lý scroll */
  });
}
window.addEventListener("scroll", optimizedScrollHandler, { passive: true });

// 4. Yield sau input, để browser paint trước rồi mới làm việc phụ
button.addEventListener("click", async () => {
  await updateCriticalUI(); // phần cần thiết ngay
  if (document.visibilityState === "visible") {
    await yieldToMain(); // nhường main thread
    await doNonCriticalWork(); // việc có thể đợi
  }
});

function yieldToMain() {
  // scheduler.yield() (experimental, Chrome 129+) là API mới nhất cho pattern này
  if ("scheduler" in window && scheduler.yield) {
    return scheduler.yield();
  }
  // Fallback: postTask với priority thấp, hoặc setTimeout 0
  return new Promise((r) => setTimeout(r, 0));
}

Scheduler API, scheduler.postTask (Chrome 94+, Firefox 129+):

Cho phép đặt task với priority rõ ràng ("user-blocking" | "user-visible" | "background") thay vì chỉ nhờ setTimeout hay requestIdleCallback:

// Render ngay phần user vừa tương tác với
scheduler.postTask(renderActiveTab, { priority: "user-blocking" });

// Warm-up cache cho tab khác, không ảnh hưởng INP
scheduler.postTask(prefetchAdjacentTabs, { priority: "background" });

// Có thể abort nếu user navigate đi chỗ khác
const controller = new TaskController();
scheduler.postTask(expensiveAnalytics, {
  priority: "background",
  signal: controller.signal,
});
// controller.abort(); // khi cần huỷ

Long Animation Frames (LoAF) API, debug INP chi tiết:

LoAF (Chrome 123+) báo cáo mỗi frame > 50ms kèm breakdown: renderStart, styleAndLayoutStart, script chiếm bao nhiêu. Đây là công cụ chính để tìm thủ phạm làm INP tệ:

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 200) {
      // Những entry này gần như chắc chắn là nguồn INP kém
      console.warn("LoAF", {
        duration: entry.duration,
        blockingDuration: entry.blockingDuration,
        renderStart: entry.renderStart - entry.startTime,
        scripts: entry.scripts?.map((s) => ({
          name: s.name,
          invoker: s.invoker,
          duration: s.duration,
          sourceURL: s.sourceURL,
        })),
      });
    }
  }
}).observe({ type: "long-animation-frame", buffered: true });

3. Cumulative Layout Shift (CLS)

Giảm thiểu sự dịch chuyển bố cục không mong muốn.

<!-- Đặt kích thước cho hình ảnh -->
<img src="image.jpg" width="800" height="600" alt="Description" />

<!-- Dự trữ không gian cho nội dung động -->
<div style="min-height: 200px;">
  <div id="dynamic-content"></div>
</div>

<!-- Tối ưu hóa font loading -->
<link
  rel="preload"
  href="/fonts/my-font.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>
<style>
  @font-face {
    font-family: "MyFont";
    font-display: swap;
    src: url("/fonts/my-font.woff2") format("woff2");
  }
</style>

4. CSS hiện đại giúp perf tự nhiên

Một số tính năng CSS đã Baseline giúp giảm JS, giảm layout thrashing, hoặc cải thiện LCP/CLS mà ít người biết:

content-visibility: auto, skip render cho phần tử ngoài viewport:

.card {
  content-visibility: auto;
  contain-intrinsic-size: auto 300px; /* gợi ý kích thước để browser không gãy scroll */
}

Rất hiệu quả cho trang dài (feed, list > 100 item), LCP + TBT đều cải thiện.

Container Queries, component tự responsive theo container, không cần viewport breakpoint. Giảm nhu cầu JS tính size / ResizeObserver:

.card-container {
  container-type: inline-size;
}

@container (min-width: 480px) {
  .card {
    display: grid;
    grid-template-columns: 1fr 2fr;
  }
}

:has() selector, parent-aware styling, Baseline 2024. Thay nhiều đoạn JS “khi con có X thì thêm class cha”:

/* Card có image thì cột trái rộng hơn */
.card:has(img) {
  grid-template-columns: 120px 1fr;
}

/* Form có input invalid thì đổi nền */
form:has(:invalid) {
  background: #fff5f5;
}

View Transitions API, animation chuyển trang/state mượt mà bằng browser, không cần framer-motion:

// Same-document: Chrome 111+, Firefox 129+, Safari 18+
if (document.startViewTransition) {
  document.startViewTransition(() => {
    updateDOMState();
  });
}
/* Custom transition cho phần tử cụ thể */
.hero {
  view-transition-name: hero;
}

::view-transition-old(hero),
::view-transition-new(hero) {
  animation-duration: 300ms;
}

Cross-document View Transitions (Chrome 126+) cho phép animation giữa hai navigation khác URL (SSG/MPA), Astro/SvelteKit đã tích hợp.

popover attribute, dialog/popover không cần JS toggle + không cần thư viện:

<button popovertarget="menu">Open menu</button>
<div id="menu" popover>
  <a href="/profile">Profile</a>
  <a href="/logout">Logout</a>
</div>

Tất cả các kỹ thuật trên giúp giảm JavaScript, giảm main-thread work → đồng nghĩa cải thiện INP và bundle size. Ưu tiên dùng platform trước khi viện tới thư viện.

Theo dõi và phân tích hiệu suất

Theo dõi hiệu suất liên tục giúp phát hiện và khắc phục vấn đề sớm.

1. Lighthouse và PageSpeed Insights

Sử dụng Lighthouse để phân tích hiệu suất trang web.

# Cài đặt Lighthouse CLI
npm install -g lighthouse

# Chạy Lighthouse
lighthouse https://example.com --view

2. Web Vitals monitoring

Theo dõi Core Web Vitals trong môi trường thực tế.

import { onCLS, onINP, onLCP, onFCP, onTTFB } from "web-vitals";

function sendToAnalytics(metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    id: "user-id",
    page: window.location.pathname,
  });

  // Sử dụng Beacon API nếu có thể
  if (navigator.sendBeacon) {
    navigator.sendBeacon("/analytics", body);
  } else {
    fetch("/analytics", {
      body,
      method: "POST",
      keepalive: true,
    });
  }
}

// Theo dõi các metrics (INP thay FID trong Core Web Vitals)
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);

3. Performance monitoring trong CI/CD

Tích hợp kiểm tra hiệu suất vào quy trình CI/CD.

# GitHub Actions workflow
name: Performance Testing

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "npm"
      - name: Install dependencies
        run: npm ci
      - name: Build
        run: npm run build
      - name: Start server
        run: npm run start & npx wait-on http://localhost:3000
      - name: Run Lighthouse
        run: |
          npm install -g @lhci/cli
          lhci autorun          

4. User-centric performance metrics

Theo dõi các metrics quan trọng đối với trải nghiệm người dùng.

// Theo dõi thời gian tương tác
const interactions = {};

document.addEventListener("click", (e) => {
  const target = e.target.closest("button, a");
  if (target) {
    const id = target.id || target.href || "unknown";
    interactions[id] = {
      startTime: performance.now(),
    };
  }
});

// Theo dõi khi UI cập nhật
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // Tìm tương tác gần nhất
    const now = performance.now();
    for (const [id, data] of Object.entries(interactions)) {
      if (now - data.startTime < 1000) {
        // Trong vòng 1 giây
        sendToAnalytics({
          name: "interaction-to-paint",
          value: now - data.startTime,
          id: id,
        });
        delete interactions[id];
      }
    }
  }
});

observer.observe({ entryTypes: ["paint"] });

Chín kỹ thuật tối ưu áp dụng được cho mọi framework

Tối ưu hóa hiệu suất là một quá trình liên tục đòi hỏi sự kết hợp của nhiều kỹ thuật. Bằng cách áp dụng các nguyên tắc trong bài viết này, bạn có thể cải thiện đáng kể hiệu suất ứng dụng frontend, bất kể framework nào bạn đang sử dụng.

Hãy nhớ rằng:

  1. Tối ưu hóa bundle size bằng tree shaking và phân tích bundle
  2. Sử dụng code-splitting và lazy-loading để giảm thời gian tải ban đầu
  3. Tối ưu hóa rendering với virtualization và tránh reflow không cần thiết
  4. Quản lý state hiệu quả để tránh re-render không cần thiết
  5. Tối ưu hóa hình ảnh và media với lazy-loading và định dạng hiện đại
  6. Sử dụng caching và memoization để tránh tính toán lại
  7. Cân nhắc SSR và SSG để cải thiện thời gian tải trang đầu tiên
  8. Tối ưu hóa Core Web Vitals (LCP, INP, CLS)
  9. Theo dõi và phân tích hiệu suất liên tục

Bằng cách kết hợp các kỹ thuật này, bạn có thể tạo ra ứng dụng frontend nhanh, mượt mà và thân thiện với người dùng, bất kể công nghệ nào bạn đang sử dụng.


Câu hỏi hay gặp

Tree shaking không hoạt động, tại sao?

Trả lời: Phổ biến nhất: library dùng CommonJS (require) thay vì ES Modules (import/export). Webpack/Rollup chỉ tree-shake ES Modules. Kiểm tra package.json của library có "module" hoặc "exports" field không. Nếu không: tìm alternative hoặc import chỉ function cần (import { debounce } from 'lodash-es').

SSR có luôn nhanh hơn CSR không?

Trả lời: Không luôn luôn. SSR nhanh hơn FCP/LCP (user thấy content sớm hơn) nhưng TTFB cao hơn (server phải render). Với SPA đơn giản + CDN, CSR có thể nhanh hơn. Dùng SSR cho content-heavy, SEO-critical; CSR cho dashboard, internal tools.

Virtual scrolling có ảnh hưởng accessibility không?

Trả lời: Có. Screen reader không thấy element chưa render. Giải pháp: dùng aria-rowcountaria-rowindex trên container và rows; cung cấp alternative view (pagination) cho assistive technology. Library như @tanstack/virtual hỗ trợ accessibility props.


Bài tiếp theo: Tối ưu hóa render trong React, Virtual DOM, memo, hooks, Server Components.