Tối ưu hóa hiệu suất cho tất cả các framework frontend

Bài viết này tập trung vào các kỹ thuật tối ưu hóa hiệu suất có thể áp dụng cho mọi framework frontend hiện đại, bao gồm React, Vue, Angular, Svelte, Solid và các framework khác. Những nguyên tắc này giúp cải thiện trải nghiệm người dùng bất kể công nghệ nào bạn đang sử dụng.

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. Tổng kết

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 VueQuery
import { useQuery } from "vue-query";

export default {
  setup() {
    const { data, isLoading, error } = useQuery("users", fetchUsers, {
      staleTime: 60000, // 1 phút
      cacheTime: 900000, // 15 phút
    });

    return { data, isLoading, error };
  },
};

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. First Input Delay (FID) và Interaction to Next Paint (INP)

Cải thiện khả năng phản hồi của trang web.

// Chia nhỏ JavaScript với dynamic import
const heavyComponent = () => import("./HeavyComponent");

// Sử dụng Web Worker cho tác vụ nặng
const worker = new Worker("./worker.js");
worker.postMessage({ data: complexData });
worker.onmessage = (e) => {
  updateUI(e.data);
};

// Tối ưu hóa event handlers
function optimizedScrollHandler() {
  if (scrollTimeout) {
    cancelAnimationFrame(scrollTimeout);
  }

  scrollTimeout = requestAnimationFrame(() => {
    // Xử lý scroll event
  });
}

window.addEventListener("scroll", optimizedScrollHandler, { passive: 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>

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 { getCLS, getFID, getLCP, getFCP, getTTFB } 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 tất cả các metrics
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
getFCP(sendToAnalytics);
getTTFB(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@v2
      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: "16"
      - 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"] });

Tổng kết

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, FID/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ác phần tiếp theo mình sẽ đề cập dến các kỹ thuật tối ưu cụ thể cho Reatjs và Vue nhé.

Và đừng quên bạn có góp gì thì để lại bình cho mình biết với nhé.