Vượt xa SPA: Tại sao Remix.js là lựa chọn hàng đầu của tôi cho các ứng dụng hiệu năng cao

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Bắt đầu: Một khung ứng dụng sẵn sàng triển khai trong 60 giây

Tôi đã dành nhiều năm vật lộn với các thư viện quản lý trạng thái (state management) cho đến khi thử dùng Remix. Nó thay thế các lớp trừu tượng phức tạp bằng những bản thiết kế gốc của web: HTTP, HTML và các hành vi gốc của trình duyệt. Trong khi nhiều framework vùi lấp web dưới các lớp trừu tượng, Remix chọn cách tận dụng chúng để xây dựng ứng dụng Web tương tác không cần quá nhiều JavaScript. Tôi đã khởi tạo dự án mới nhất của mình trong chưa đầy một phút bằng CLI của họ.

npx create-remix@latest

Trình cài đặt sẽ hỏi bạn về mục tiêu triển khai như Vercel, Cloudflare hoặc Fly.io. Với hầu hết các dự án, Remix App Server mặc định cung cấp một điểm khởi đầu vững chắc. Sau khi cài đặt hoàn tất, bạn sẽ thấy một cấu trúc dự án gọn gàng. Thư mục app/ đóng vai trò là trung tâm điều khiển, chứa mọi route, style và component giúp bạn xây dựng logic Type-safe với TypeScript nâng cao.

Lộ trình: Routing dựa trên tệp tin một cách logic

Hãy coi thư mục app/routes/ là lộ trình cho ứng dụng của bạn. Remix sử dụng file-based routing, vì vậy tên tệp của bạn sẽ được chuyển đổi trực tiếp thành đường dẫn URL. Một tệp được tạo tại app/routes/dashboard.tsx sẽ ngay lập tức hoạt động tại /dashboard. Việc ánh xạ có thể dự đoán được này giúp ngay cả những codebase khổng lồ cũng trở nên dễ quản lý khi bạn tìm kiếm một view hoặc khối logic cụ thể.

Kiến trúc cốt lõi: Loaders và Actions

Xử lý dữ liệu đại diện cho sự thay đổi tư duy lớn nhất khi chuyển sang Remix. Cuối cùng, chúng ta có thể từ bỏ mớ hỗn độn “useEffect-fetch” waterfall vốn làm chậm rất nhiều ứng dụng React. Remix đơn giản hóa việc này bằng cách giới thiệu hai hàm chính: loaderaction.

Loaders: Những người gác cổng phía Server

Loaders là những người gác cổng phía server của bạn. Chúng chỉ chạy trên server, cho phép bạn truy vấn cơ sở dữ liệu bằng các công cụ hiện đại như Drizzle ORM hoặc gọi các API bên ngoài một cách an toàn. Vì client không bao giờ thấy mã này, bạn có thể sử dụng các biến môi trường riêng tư mà không sợ bị lộ. Trong một dự án gần đây, việc chuyển fetch dữ liệu sang loader đã giúp giảm 15% kích thước bundle ban đầu và cải thiện Largest Contentful Paint (LCP) thêm 450ms.

import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { getPosts } from "~/models/post.server";

export const loader = async () => {
  const posts = await getPosts();
  return json({ posts });
};

export default function Posts() {
  const { posts } = useLoaderData<typeof loader>();
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.slug}>{post.title}</li>
      ))}
    </ul>
  );
}

Việc fetch dữ liệu trên server giúp loại bỏ tình trạng nhảy bố cục (layout shift) gây khó chịu. Người dùng nhận được một tài liệu HTML đầy đủ nội dung ngay lập tức. Đây không chỉ là điểm cộng cho SEO; nó còn giúp trang web của bạn có thể sử dụng được trên các kết nối 3G chậm, nơi mà một bundle JS nặng có thể mất tới 5 giây để thực thi.

Actions: Xử lý ý định của người dùng

Nếu loaders đảm nhận việc đọc, thì actions quản lý việc ghi. Chúng xử lý các yêu cầu POST, PUT và DELETE bằng cách sử dụng các form HTML tiêu chuẩn. Khi người dùng nhấn gửi, Remix sẽ xử lý yêu cầu trên server, sau đó tự động kích hoạt quá trình xác thực lại (revalidation) cho tất cả các loaders đang hoạt động. Điều này đảm bảo giao diện người dùng luôn đồng bộ hoàn hảo với cơ sở dữ liệu mà không cần quản lý trạng thái thủ công.

export const action = async ({ request }: ActionFunctionArgs) => {
  const formData = await request.formData();
  const title = formData.get("title");
  // Logic cơ sở dữ liệu được thực hiện tại đây
  return redirect("/posts");
};

Các mô hình nâng cao: Nested Routing và Optimistic UI

Nested routing là siêu năng lực tiềm ẩn của Remix. Nó không chỉ là về giao diện hiển thị thế nào, mà còn là về cách dữ liệu luân chuyển. Remix có thể fetch dữ liệu cho nhiều phân đoạn route đang hoạt động cùng một lúc và song song.

Tại sao Nested Routes lại quan trọng

Hãy tưởng tượng một bảng điều khiển (dashboard) có thanh bên (sidebar) và khu vực nội dung chính. Việc nhấp vào một liên kết trong sidebar thường buộc toàn bộ trang phải tải lại hoặc yêu cầu các bản cập nhật trạng thái toàn cục phức tạp trong các framework truyền thống. Trong Remix, chỉ phân đoạn nested route cụ thể mới thay đổi. Sự chính xác này giúp giảm tới 70% lượng dữ liệu truyền tải trong một số view của tôi, khiến quá trình chuyển đổi có cảm giác tức thì.

Triển khai Optimistic UI

Các biểu tượng tải (loading spinners) là một thất bại về trải nghiệm người dùng (UX). Remix cung cấp hook useFetcher để triển khai Optimistic UI. Điều này cho phép bạn cập nhật giao diện như thể yêu cầu từ server đã thành công. Nếu cuối cùng server trả về lỗi, Remix sẽ xử lý việc hoàn tác (rollback) cho bạn. Ứng dụng của bạn sẽ mang lại cảm giác như một công cụ máy tính cục bộ, ngay cả khi nó đang giao tiếp với một cơ sở dữ liệu cách xa hàng ngàn dặm.

Hiệu năng và các phương pháp hay nhất cho thực tế

Xây dựng một ứng dụng chỉ là bước đầu tiên. Hiệu năng thực sự trong Remix đến từ việc sử dụng các công cụ có sẵn của nền tảng web thay vì chồng chất thêm JavaScript.

Caching là người bạn tốt nhất của bạn

Remix giúp việc thiết lập HTTP cache headers trở nên đơn giản. Bạn có thể export một hàm headers từ bất kỳ route nào để hướng dẫn trình duyệt và CDN cách lưu trữ nội dung của bạn, giúp giảm mạnh độ trễ API với Cache-Control và ETags. Điều này cực kỳ quan trọng đối với các blog có lưu lượng truy cập cao hoặc các trang thương mại điện tử. Nó mang lại cho bạn tốc độ của một trang web tĩnh cùng với sức mạnh của một server động.

export const headers = () => ({
  "Cache-Control": "public, max-age=3600, s-maxage=86400",
});

Khả năng phục hồi với Error Boundaries

Một component bị lỗi không nên làm sập toàn bộ ứng dụng của bạn. Remix giải quyết vấn đề này bằng Error Boundaries. Bạn có thể định nghĩa một ErrorBoundary cho bất kỳ route nào. Nếu một nested route bị lỗi, chỉ phần đó mới hiển thị thông báo lỗi. Phần còn lại của ứng dụng vẫn có thể tương tác đầy đủ, đây là một điểm cộng lớn cho các bảng điều khiển phức tạp và nặng về dữ liệu.

export function ErrorBoundary() {
  return (
    <div className="error-container">
      <h2>Đã có lỗi xảy ra tại đây.</h2>
      <p>Phần còn lại của bảng điều khiển vẫn đang hoạt động bình thường.</p>
    </div>
  );
}

Chọn “ngôi nhà” cho ứng dụng của bạn

Khi bạn đã sẵn sàng triển khai, hãy chọn mục tiêu triển khai phù hợp với nhu cầu. Nếu độ trễ thấp là ưu tiên hàng đầu, Cloudflare Workers hoặc Deno 2.0 cung cấp một môi trường edge computing tuyệt vời. Đối với các ứng dụng nặng về cơ sở dữ liệu, tôi thích Fly.io hơn. Nó hỗ trợ triển khai đa vùng và SQLite, kết hợp hoàn hảo với bản chất hướng server của Remix.

Việc đánh đổi một SPA phía client để lấy một framework tập trung vào server đòi hỏi một sự thay đổi trong cách nhìn nhận. Tuy nhiên, kết quả là không thể phủ nhận. Bạn sẽ có thời gian tải nhanh hơn, mã nguồn sạch hơn và trải nghiệm nhà phát triển mang lại cảm giác gắn liền với những điểm mạnh cơ bản của web.

Share: