Bài 30.4: Font Optimization trong TypeScript

Trong thế giới lập trình web hiện đại, hiệu suất là một yếu tố tối quan trọng, không chỉ ảnh hưởng đến thứ hạng SEO mà còn tác động trực tiếp đến trải nghiệm người dùng. Một trong những tài nguyên thường gây chậm trễ khi tải trang chính là font chữ tùy chỉnh. Chúng ta sử dụng font để làm cho trang web của mình trở nên độc đáo và hấp dẫn hơn, nhưng nếu không được tối ưu hóa đúng cách, chúng có thể trở thành nút cổ chai, khiến nội dung bị giật (FOIT - Flash of Invisible Text, FOUT - Flash of Unstyled Text) hoặc hiển thị chậm.

Trong bài viết này, chúng ta sẽ khám phá cách tối ưu hóa việc tải và sử dụng font chữ trong các dự án phát triển bằng TypeScript, một ngôn ngữ ngày càng phổ biến trong cộng đồng front-end. Mặc dù tối ưu hóa font chủ yếu liên quan đến CSS và cấu hình build tool, TypeScript đóng vai trò quan trọng trong việc quản lý code, tích hợp các kỹ thuật tối ưu, và cung cấp tính an toàn kiểu dữ liệu khi làm việc với các tài nguyên này.

Tại sao tối ưu hóa Font lại quan trọng?

Font chữ tùy chỉnh (web fonts) cần được tải về từ server của bạn hoặc từ một CDN bên ngoài (như Google Fonts). Quá trình này tốn thời gian và băng thông. Nếu trình duyệt phải đợi font tải xong mới hiển thị văn bản, người dùng sẽ thấy một trang trống hoặc văn bản bị ẩn (FOIT). Ngược lại, nếu văn bản hiển thị trước bằng font mặc định rồi mới chuyển sang font tùy chỉnh (FOUT), điều này có thể gây ra hiện tượng layout shift (nội dung bị dịch chuyển) rất khó chịu.

Mục tiêu của tối ưu hóa font là:

  1. Giảm kích thước font: Tải ít dữ liệu hơn.
  2. Tăng tốc độ tải: Giúp font sẵn sàng nhanh hơn.
  3. Kiểm soát hành vi hiển thị: Tránh FOIT và giảm thiểu FOUT/layout shift.

Các Kỹ thuật Tối Ưu Hóa Font Phổ Biến

Trước khi đi vào vai trò của TypeScript, hãy điểm qua các kỹ thuật tối ưu hóa font cơ bản mà chúng ta sẽ áp dụng:

  • Sử dụng định dạng font hiện đại: WOFF2 là định dạng được khuyến nghị nhất hiện nay vì nó cung cấp khả năng nén vượt trội so với WOFF, TTF, hoặc OTF. Luôn cung cấp các định dạng font khác làm fallback cho các trình duyệt cũ.
  • Subsetting (Cắt bỏ ký tự không dùng): Nếu bạn chỉ sử dụng một phần nhỏ các ký tự trong font (ví dụ: chỉ chữ cái Latin cơ bản, không có ký tự đặc biệt hoặc ngôn ngữ khác), bạn có thể tạo ra một phiên bản font chỉ chứa các ký tự đó. Điều này giảm đáng kể kích thước file font.
  • Sử dụng font-display trong CSS: Thuộc tính CSS này cho phép bạn kiểm soát cách văn bản hiển thị trong khi font tùy chỉnh đang được tải. Các giá trị phổ biến:
    • swap: Văn bản hiển thị ngay lập tức bằng font fallback, sau khi font tùy chỉnh tải xong sẽ đổi sang font đó. (Tránh FOIT, nhưng gây FOUT).
    • fallback: Cung cấp một khoảng thời gian chờ rất ngắn, sau đó hiển thị font fallback. Nếu font tùy chỉnh tải xong trong thời gian chờ, nó sẽ được sử dụng. (Ít gây FOUT hơn swap).
    • optional: Cung cấp thời gian chờ cực ngắn. Nếu font tải xong, nó được sử dụng. Nếu không, trình duyệt sẽ chỉ dùng font fallback cho lần truy cập này. Font tùy chỉnh sẽ chỉ được tải trong nền và dùng cho các lần truy cập sau (sau khi được cache). (Tốt nhất cho hiệu suất, nhưng có thể không hiển thị font tùy chỉnh ở lần đầu).
  • Preload font: Sử dụng <link rel="preload" as="font" ...> trong HTML để yêu cầu trình duyệt tải font sớm nhất có thể, trước khi nó gặp CSS cần dùng font đó. Điều này giúp font sẵn sàng nhanh hơn.
  • Tự host font: Tải font về server của bạn thay vì dùng CDN bên ngoài. Điều này giúp tránh một request DNS lookup và có thể tận dụng kết nối HTTP/2 hiệu quả hơn. Tuy nhiên, CDN lớn có thể có cache tốt hơn và server ở gần người dùng hơn. Cần cân nhắc kỹ.
  • Sử dụng các font hệ thống (System Fonts): Đối với một số phần của website, việc sử dụng font có sẵn trên hệ điều hành của người dùng có thể là một giải pháp tối ưu nhất về hiệu suất (vì không phải tải gì cả).

Tích hợp Tối Ưu Hóa Font trong Dự án TypeScript

Vậy TypeScript phù hợp ở đâu trong bức tranh này? TypeScript không trực tiếp tối ưu hóa file font (như nén hay subset), nhưng nó là ngôn ngữ mà bạn sử dụng để xây dựng ứng dụng front-end và cấu hình các công cụ build. Do đó, bạn sẽ sử dụng TypeScript để:

  1. Cấu hình Build Tools: Các công cụ như Webpack, Vite, Rollup, thường được cấu hình bằng các file .js hoặc .ts. Đây là nơi bạn thiết lập cách các file font được xử lý (copy, nén, chuyển đổi định dạng, subsetting).
  2. Quản lý Import Font: Trong các module hệ thống hiện đại, bạn có thể "import" các file tài nguyên như font. TypeScript giúp bạn định nghĩa kiểu cho các import này.
  3. Thực hiện Tải Font Động (Dynamic Font Loading): Trong một số trường hợp nâng cao, bạn có thể muốn tải font dựa trên tương tác của người dùng hoặc dữ liệu trên trang. TypeScript/JavaScript là ngôn ngữ để làm điều này.
  4. Làm việc với các Framework: Các framework như Next.js, Remix (thường dùng với TS) cung cấp các cơ chế tích hợp sẵn để tối ưu hóa font.

Chúng ta hãy xem xét từng khía cạnh này với các ví dụ code minh họa (ngắn gọn).

1. Cấu hình Build Tools với TypeScript

Các dự án TypeScript hiện đại thường sử dụng các module bundler. Cấu hình của chúng thường nằm trong các file như webpack.config.ts, vite.config.ts, v.v. Đây là nơi bạn chỉ định cách xử lý các file font.

Ví dụ với Webpack (phiên bản 5+, sử dụng Asset Modules):

// webpack.config.ts
import path from 'path';
import type { Configuration } from 'webpack'; // Sử dụng kiểu dữ liệu từ webpack

const config: Configuration = {
  // ... các cấu hình khác
  module: {
    rules: [
      // ... các rules khác (cho .ts, .tsx, .css, ...)
      {
        test: /\.(woff2?|eot|ttf|otf)$/i, // Tìm các file font
        type: 'asset/resource', // Sử dụng Asset Modules của Webpack 5
        generator: {
          // Đặt tên file output theo cấu trúc mong muốn, thêm hash để cache busting
          filename: 'fonts/[name].[hash][ext]',
        },
      },
    ],
  },
  // ...
};

export default config;

Giải thích:

  • File cấu hình được viết bằng TypeScript (webpack.config.ts), sử dụng các kiểu dữ liệu (Configuration) từ thư viện webpack để cung cấp tính năng tự hoàn thành (autocomplete) và kiểm tra lỗi kiểu trong editor của bạn. Đây là lợi ích của việc dùng TS cho config.
  • test: /\.(woff2?|eot|ttf|otf)$/i khớp với các định dạng font phổ biến.
  • type: 'asset/resource' trong Webpack 5 sẽ tự động sao chép file font vào thư mục output và trả về URL của file đó.
  • generator.filename tùy chỉnh đường dẫn và tên file output.

Với Vite, cấu hình font thường đơn giản hơn vì nó sử dụng Rollup bên dưới và có các quy tắc mặc định tốt:

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  // Vite mặc định sẽ xử lý các file asset như font, sao chép chúng vào thư mục output
  // và thêm hash.
  // Bạn có thể cấu hình sâu hơn nếu cần, ví dụ:
  build: {
    assetsInlineLimit: 0, // Không inline font vào base64 (thường font lớn)
  }
});

Giải thích:

  • Sử dụng defineConfig giúp nhận được gợi ý kiểu dữ liệu khi viết cấu hình Vite.
  • Vite mặc định đã xử lý font như các tài nguyên khác. Tùy chọn assetsInlineLimit: 0 đảm bảo font không bị biến thành chuỗi base64 trong bundle CSS (vì font thường có kích thước lớn, inline sẽ làm file CSS rất lớn).

Lợi ích của TS ở đây: Cung cấp an toàn kiểu dữ liệuhỗ trợ editor khi viết các file cấu hình phức tạp, giảm thiểu lỗi cú pháp hoặc lỗi logic liên quan đến kiểu.

2. Quản lý Import Font với TypeScript

Trong các hệ thống module, bạn có thể import font trực tiếp vào file TS/JS của mình. Tuy nhiên, trình duyệt không thể trực tiếp sử dụng kết quả của việc import này (nó là một đường dẫn URL). Import này chủ yếu để báo cho build tool biết cần xử lý file font đó.

Để TypeScript không báo lỗi khi bạn import các file non-code như .woff2, bạn cần khai báo kiểu cho chúng trong file .d.ts:

// src/types/assets.d.ts hoặc chỉ đơn giản là global.d.ts
declare module '*.woff';
declare module '*.woff2';
declare module '*.ttf';
declare module '*.otf';
declare module '*.eot';

// Bạn có thể mở rộng cho các loại tài nguyên khác
// declare module '*.svg';
// declare module '*.png';
// declare module '*.jpg';
// declare module '*.jpeg';
// declare module '*.gif';

Giải thích:

  • File này không chứa code thực thi, chỉ chứa các khai báo kiểu.
  • declare module '*.woff2'; nói với TypeScript rằng khi bạn import một file kết thúc bằng .woff2, kết quả import đó có kiểu dữ liệu bất kỳ (any), và quan trọng là không báo lỗi.

Sau đó, bạn có thể import font trong code TS/JS (mặc dù không sử dụng giá trị import này trực tiếp trong logic):

// src/App.tsx (ví dụ với React)
import React from 'react';
import './App.css'; // CSS file sẽ chứa @font-face

// Import này chỉ để đảm bảo build tool biết về file font
import myFontWoff2 from './assets/fonts/MyFont.woff2';
import myFontWoff from './assets/fonts/MyFont.woff';

function App() {
  return (
    <div className="App">
      <h1>Chào mừng bạn đến với Blog của FullhouseDev!</h1>
      <p>Đây  nội dung sử dụng font tùy chỉnh.</p>
    </div>
  );
}

export default App;
/* src/App.css */
@font-face {
  font-family: 'MyCustomFont';
  src: url('./assets/fonts/MyFont.woff2') format('woff2'),
       url('./assets/fonts/MyFont.woff') format('woff');
  font-weight: normal;
  font-style: normal;
  font-display: swap; /* Quan trọng cho hiệu suất */
}

.App h1, .App p {
  font-family: 'MyCustomFont', sans-serif; /* Thêm font fallback */
}

Giải thích:

  • Code TypeScript import myFontWoff2 from './assets/fonts/MyFont.woff2'; chỉ đóng vai trò là một "báo hiệu" cho build tool (Webpack/Vite) rằng file này đang được sử dụng và cần được xử lý (ví dụ: sao chép vào thư mục dist và trả về đường dẫn mới). Bạn thường không cần sử dụng biến myFontWoff2 này trong code TS/JS.
  • Việc sử dụng font thực sự diễn ra trong CSS với @font-face. Build tool sẽ thay thế url('./assets/fonts/MyFont.woff2') bằng đường dẫn đến file font đã được xử lý trong thư mục dist (ví dụ: /fonts/MyFont.abc123.woff2).
  • font-display: swap; được thêm vào trong CSS để kiểm soát hành vi hiển thị, tránh FOIT.

Lợi ích của TS ở đây: Cung cấp type safety cho quá trình import tài nguyên không phải code, giúp build process đáng tin cậy hơn.

3. Tải Font Động với TypeScript/JavaScript

Trong một số trường hợp, bạn có thể muốn tải font chữ sau khi trang đã tải xong hoặc khi người dùng thực hiện một hành động cụ thể. Ví dụ: tải font cho một trình soạn thảo văn bản chỉ khi người dùng mở nó, hoặc tải font cho một ngôn ngữ chỉ khi người dùng chuyển ngôn ngữ. API Font Loading của trình duyệt là công cụ mạnh mẽ cho việc này, và bạn có thể sử dụng TypeScript để triển khai nó.

// src/utils/fontLoader.ts

/**
 * Tải một font tùy chỉnh sử dụng Font Loading API.
 * @param name Tên font (font-family).
 * @param url URL của file font (ví dụ: .woff2).
 * @returns Promise hoàn thành khi font đã tải xong.
 */
async function loadCustomFont(name: string, url: string): Promise<void> {
  if (!(document as any).fonts) {
    console.warn('Font Loading API not supported.');
    return; // API không được hỗ trợ
  }

  const font = new FontFace(name, `url(${url})`);

  try {
    // Tải font
    const loadedFont = await font.load();

    // Thêm font vào Document.fonts
    (document as any).fonts.add(loadedFont);

    console.log(`Font "${name}" loaded successfully.`);

    // Kích hoạt layout lại để áp dụng font mới (tùy chọn, thường trình duyệt làm tự động)
    // document.body.style.display = 'none';
    // document.body.offsetHeight; // Trigger reflow
    // document.body.style.display = '';

  } catch (error) {
    console.error(`Failed to load font "${name}":`, error);
  }
}

// Ví dụ sử dụng trong component React (hoặc bất kỳ đâu cần tải động)
// import { useEffect } from 'react';
// import { loadCustomFont } from './utils/fontLoader';
// import fancyFontWoff2 from './assets/fonts/FancyFont.woff2'; // Import đường dẫn từ build tool

// function MyComponent() {
//   useEffect(() => {
//     // Tải font khi component mount
//     loadCustomFont('FancyCustomFont', fancyFontWoff2);
//   }, []);

//   return (
//     <div style={{ fontFamily: 'FancyCustomFont, serif' }}>
//       Nội dung sử dụng font tải động.
//     </div>
//   );
// }

Giải thích:

  • Chúng ta định nghĩa một hàm loadCustomFont bằng TypeScript.
  • Kiểm tra sự tồn tại của document.fonts (Font Loading API). (document as any).fonts được dùng để tránh lỗi kiểu nếu môi trường TypeScript không có khai báo type cho document.fonts (thường có trong các cài đặt TS hiện đại).
  • Tạo một đối tượng FontFace.
  • Gọi phương thức font.load() bất đồng bộ để tải font.
  • Sử dụng await để đợi quá trình tải hoàn tất.
  • Thêm font đã tải vào document.fonts, làm cho nó sẵn sàng để sử dụng trong CSS.
  • try...catch xử lý lỗi nếu quá trình tải thất bại.

Lợi ích của TS ở đây:

  • Cung cấp type safety cho các tham số truyền vào hàm (name là string, url là string).
  • Giúp quản lý code bất đồng bộ (async/await, Promise) một cách rõ ràng và ít lỗi hơn.
  • Dễ dàng tích hợp logic tải font động này vào các component hoặc service của ứng dụng TS của bạn.
4. Làm việc với Framework (Ví dụ: Next.js)

Các framework như Next.js, được sử dụng rộng rãi với TypeScript, cung cấp các giải pháp tích hợp sẵn để tối ưu hóa font một cách tự động và hiệu quả. @next/font là một ví dụ điển hình.

// src/pages/_app.tsx (ví dụ với Next.js App Router)
import type { AppProps } from 'next/app';
import localFont from '@next/font/local'; // Import font cục bộ
import { Inter } from 'next/font/google'; // Import font từ Google Fonts

// Cấu hình font cục bộ
const myLocalFont = localFont({
  src: '../path/to/myfont.woff2', // Đường dẫn tương đối từ file này
  display: 'swap', // Sử dụng font-display: swap
  weight: '400',
  style: 'normal',
  variable: '--my-local-font' // Định nghĩa biến CSS
});

// Cấu hình font từ Google Fonts
const inter = Inter({
  subsets: ['latin'], // Chỉ tải subset Latin
  display: 'swap', // Sử dụng font-display: swap
  variable: '--font-inter', // Định nghĩa biến CSS
});

function MyApp({ Component, pageProps }: AppProps) {
  return (
    // Áp dụng các lớp CSS hoặc biến CSS global
    <div className={`${myLocalFont.variable} ${inter.variable}`}>
      <Component {...pageProps} />
    </div>
  );
}

export default MyApp;

Giải thích:

  • Next.js cung cấp module @next/font giúp bạn import font (cả cục bộ và từ Google Fonts).
  • Bạn cấu hình font bằng các hàm của @next/font, chỉ định đường dẫn file (src), subset, font-display, v.v.
  • Next.js sẽ tự động:
    • Tối ưu hóa font (subsetting, chuyển đổi định dạng sang WOFF2 nếu có thể).
    • Tự động host font (đối với Google Fonts nữa!).
    • Thêm các <link rel="preload"> cần thiết.
    • Thêm CSS @font-face với font-display đã cấu hình.
    • Cung cấp các lớp CSS hoặc biến CSS để bạn dễ dàng sử dụng font trong ứng dụng.
  • Code được viết hoàn toàn bằng TypeScript, tận dụng type checking của Next.js và @next/font.

Lợi ích của TS ở đây: Giúp sử dụng các tính năng tối ưu hóa của framework một cách có kiểu dữ liệu, rõ ràng và giảm thiểu lỗi cú pháp/logic. Framework lo phần lớn công việc tối ưu hóa phức tạp cho bạn.

Tổng kết Vai trò của TypeScript

Như bạn thấy, TypeScript không phải là công cụ trực tiếp nén hay xử lý file font ở mức bit. Vai trò của nó trong Font Optimization nằm ở việc:

  • Tăng cường độ tin cậy khi cấu hình các build tool xử lý font (như Webpack, Vite).
  • Cung cấp an toàn kiểu dữ liệu khi import các tài nguyên non-code.
  • Giúp quản lý code triển khai các kỹ thuật tải font động bằng JavaScript/TypeScript một cách có tổ chức và dễ bảo trì hơn.
  • Hỗ trợ sử dụng các API (như Font Loading API) và các tính năng tối ưu hóa font của framework một cách hiệu quả.

Tóm lại, khi làm việc với tối ưu hóa font trong một dự án TypeScript, bạn sẽ kết hợp kiến thức về CSS (@font-face, font-display), HTML (preload), cấu hình build tool, và cuối cùng là sử dụng TypeScript để viết code kết nối tất cả lại với nhau và mang lại tính mạnh mẽ, dễ bảo trì cho ứng dụng của bạn.

Các Lưu Ý Thêm về Tối Ưu Hóa Font

  • Font Fallback: Luôn định nghĩa một danh sách font fallback (ví dụ: font-family: 'MyCustomFont', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;) để đảm bảo văn bản vẫn hiển thị ngay cả khi font tùy chỉnh gặp vấn đề.
  • Kiểm tra trên nhiều trình duyệt và thiết bị: Hành vi tải và hiển thị font có thể khác nhau.
  • Đo lường hiệu suất: Sử dụng các công cụ như Google PageSpeed Insights, Lighthouse, hoặc WebPageTest để đo lường tác động của font đến hiệu suất trang web của bạn. Đặc biệt chú ý đến các chỉ số như Largest Contentful Paint (LCP) và Cumulative Layout Shift (CLS), vì font thường ảnh hưởng trực tiếp đến chúng.

Comments

There are no comments at the moment.