Mỗi lần bạn gõ một địa chỉ vào trình duyệt, bạn đang viết một "bức thư" với đầy đủ thông tin để Internet biết phải giao nó đến đâu. Hãy cùng "mổ xẻ" từng phần của URL:
Ở phần này, chúng ta sẽ xem cách browser và server nói chuyện với nhau thông qua URL: từ cách chọn absolute/relative URL, encode dữ liệu an toàn, đến việc xác định origin. Khi đã nắm origin, bạn sẽ hiểu vì sao CORS xuất hiện và vì sao một request nhìn đúng cú pháp vẫn có thể bị browser chặn lại.
URL có hai dạng chính: absolute (tuyệt đối) và relative (tương đối). Phân biệt rõ hai loại này là điều kiện tiên quyết khi làm việc với API, routing, và deploy ứng dụng.
| Dạng URL | Ví dụ | Khi nào dùng |
|---|---|---|
| Absolute | https://api.sydexa.com/users | Gọi API ngoài, linh hoạt khi deploy nhiều môi trường |
| Root-relative | /users/search | Link nội bộ trong cùng origin - phổ biến nhất với React Router |
| Path-relative | ./details hoặc ../profile | Ít dùng hơn, resolve dựa vào URL hiện tại. Dễ nhầm khi nested route sâu. |
React Router 6+ hỗ trợ cả root-relative và path-relative. Khi dùng path-relative (<Link to="./edit">), đường dẫn resolve dựa vào route hiện tại chứ không phải URL bar. Vì vậy hãy ưu tiên root-relative (/users/edit) nếu bạn muốn rõ ràng và dễ đoán.
URL chỉ cho phép một tập ký tự hạn chế (chữ cái, số, và một số ký tự đặc biệt). Ký tự không hợp lệ (khoảng trắng, dấu tiếng Việt, ký tự đặc biệt) phải được mã hóa (encode) thành dạng %XX.
Trong URL, mọi query param đều là string. Đây là nguồn lỗi thường gặp khi dev mới làm việc với URL.
Origin của một URL bao gồm ba phần: protocol + host + port. Hai URL có cùng origin (same-origin) khi cả ba phần này giống nhau.
| URL A | URL B | Same-origin? | Lý do |
|---|---|---|---|
| https://sydexa.com/blog | https://sydexa.com/api | Có | Cùng protocol, host, port (443) |
| https://sydexa.com | http://sydexa.com | Không | Khác protocol (https vs http) |
| https://sydexa.com | https://api.sydexa.com | Không | Khác host (subdomain khác = host khác) |
| http://localhost:3000 | http://localhost:8080 | Không | Khác port |
CORS (Cross-Origin Resource Sharing) là cơ chế bảo mật của trình duyệt. Khi JavaScript thực hiện request đến origin khác, browser sẽ kiểm tra response headers từ server (đặc biệt là Access-Control-Allow-Origin) để quyết định cho phép hay chặn request.
Trong development, React app chạy ở localhost:5173 (Vite) trong khi API server ở localhost:8080 → đây là cross-origin (khác port). Giải pháp: cấu hình proxy trong Vite/CRA để dev server chuyển tiếp request, hoặc cấu hình CORS headers phía backend.
Hãy tưởng tượng DNS giống như một "cuốn danh bạ điện thoại của Internet". Con người chúng ta nhớ các tên miền dễ đọc (như sydexa.com), nhưng máy tính lại giao tiếp bằng các con số gọi là địa chỉ IP (như 93.184.216.34). DNS làm nhiệm vụ dịch từ tên sang số.
Khi bạn chạy npm start và mở localhost:3000, toàn bộ quy trình DNS → TCP → HTTP → Render vẫn diễn ra, chỉ là server nằm ngay trên máy bạn (localhost). Khi deploy lên production, React app của bạn sẽ đi qua đúng 6 bước này để đến tay người dùng.
Ở phần trước, ta đã biết DNS dịch tên miền thành IP. Bây giờ hãy đi sâu vào từng bước của quá trình này - từ lúc bạn gõ URL cho đến khi trình duyệt có được địa chỉ IP.
Khi bạn truy cập sydexa.com, trình duyệt không lập tức hỏi DNS server. Nó kiểm tra qua nhiều lớp cache trước:
Đây là bước quan trọng nhất. Recursive resolver thực hiện truy vấn thay cho client - client chỉ cần gửi một yêu cầu duy nhất và chờ kết quả cuối cùng. Tuy nhiên, bản thân resolver thực hiện chuỗi truy vấn lặp (iterative) qua các tầng DNS server:
Sau khi có IP, resolver cache kết quả theo TTL rồi trả về cho client. Lần truy cập tiếp theo sẽ nhanh hơn rất nhiều nhờ cache này.
Thuật ngữ "recursive" ở đây nói về vai trò của resolver đối với client: client gửi một câu hỏi, resolver lo hết phần còn lại. Bản thân resolver thường thực hiện các truy vấn iterative (lặp từng bước) đến Root → TLD → Authoritative. Đừng nhầm "recursive lookup" nghĩa là resolver gọi đệ quy vào chính nó.
DNS không chỉ lưu trữ ánh xạ tên miền → IP. Mỗi domain có thể có nhiều loại DNS Record (bản ghi DNS) khác nhau, phục vụ các mục đích khác nhau:
| Record | Mục đích | Ví dụ giá trị |
|---|---|---|
| A | Ánh xạ domain → IPv4 address | sydexa.com → 93.184.216.34 |
| AAAA | Ánh xạ domain → IPv6 address | sydexa.com → 2606:2800:220:1:248:1893:25c8:1946 |
| CNAME | Alias - trỏ domain này sang domain khác | www.sydexa.com → sydexa.com |
| NS | Chỉ định nameserver có thẩm quyền cho domain | sydexa.com → ns1.cloudflare.com |
| MX | Chỉ định mail server nhận email cho domain | sydexa.com → mail.google.com (priority 10) |
| TXT | Lưu text tùy ý - thường dùng cho xác minh domain, SPF, DKIM | v=spf1 include:_spf.google.com ~all |
Theo chuẩn DNS (RFC 1034), CNAME record không thể tồn tại cùng các record khác tại cùng một tên (name). Ví dụ: nếu api.sydexa.com đã có CNAME, bạn không thể thêm A record hay MX record cho cùng tên đó. Đây là một trong những lý do nhiều DNS provider cung cấp record đặc biệt như ALIAS hay ANAME để vượt qua hạn chế này ở apex domain (root domain).
Mỗi DNS record đi kèm một giá trị TTL (Time To Live) - đây là thời gian (tính bằng giây) mà kết quả DNS được phép cache trước khi phải tra cứu lại.
DNS Propagation (lan truyền DNS) là quá trình các DNS resolver trên toàn thế giới dần cập nhật cache khi bạn thay đổi DNS record. Đây không phải quá trình đồng bộ tức thì - mỗi resolver cập nhật theo TTL riêng, nên có nơi thấy IP mới sớm, có nơi vẫn dùng IP cũ cho đến khi cache hết hạn.
DNS lookup là bước đầu tiên trước khi trình duyệt có thể bắt đầu tải bất kỳ tài nguyên nào. Trong waterfall (biểu đồ tải trang), DNS resolution thường chiếm 20-120ms cho mỗi domain mới. Với trang web tải tài nguyên từ nhiều domain (CDN, API, analytics...), DNS có thể là bottleneck đáng kể.
Hai kỹ thuật quan trọng giúp tối ưu:
| Kỹ thuật | Thực hiện | Khi nào dùng |
|---|---|---|
| dns-prefetch | Chỉ resolve DNS | Domain sẽ dùng sau (không urgent), hoặc khi có nhiều domain third-party |
| preconnect | DNS + TCP + TLS | Domain chắc chắn dùng ngay (API chính, font CDN). Giới hạn 2-3 domain để tránh lãng phí connection. |
Khi React app của bạn gọi API từ domain khác (ví dụ: api.sydexa.com), thêm <link rel="preconnect"> cho domain API trong file index.html giúp giảm thời gian chờ cho request đầu tiên. Với Next.js, bạn có thể đặt nó trong <Head> component.
DNS được thiết kế từ những ngày đầu của Internet, khi bảo mật chưa phải ưu tiên hàng đầu. Một số vấn đề bảo mật cần biết:
Chuỗi kiểm tra DNS cache trước khi tra cứu bên ngoài?
Browser DNS Cache → OS DNS Cache → Recursive Resolver Cache. Chỉ khi tất cả đều miss, resolver mới truy vấn Root → TLD → Authoritative.
Từ quy trình 6 giai đoạn rendering ở trên, có vẻ như mọi thứ rất "thông thoáng": HTML → DOM → Render Tree → Layout → Paint → Screen. Nhưng, quá trình render này bị cắt ngang nếu trình duyệt gặp thẻ <script>. Đây là lý do tại sao JavaScript có thể chặn việc rendering.
Để thấy điểm nghẽn nằm ở đâu, hãy xem 5 giai đoạn mà một file JavaScript phải trải qua trước khi nó thật sự chạy được trên trang. Mỗi giai đoạn đều có thể làm browser chậm lại nếu script quá lớn hoặc được tải sai thời điểm.
Giai đoạn 1: Download
Nếu trình duyệt đang đọc file HTML và gặp thẻ <script src="app.js"> thì nó sẽ tạm dừng việc phân tích HTML (Pause HTML parsing), gửi yêu cầu tải file app.js về. Đây là lý do tại sao chúng ta thường để thẻ <script> ở cuối <body> hoặc dùng defer/async để tránh chặn việc hiển thị nội dung đầu tiên (First Paint).
defer: tải song song, thực thi theo thứ tự sau khi HTML parse xong. async: tải song song, thực thi ngay khi tải xong (có thể giữa lúc parse HTML).
Các build tool hiện đại (Vite, Next.js...) sử dụng defer/async và code splitting để tối ưu script loading.
Giai đoạn 2: Parse JS → AST (Abstract Syntax Tree)
Sau khi tải xong, trình duyệt không hiểu ngay code của bạn (ví dụ: const a = 5). Nó phải chuyển đổi văn bản đó thành một cấu trúc dữ liệu dạng cây gọi là AST (Cây cú pháp trừu tượng). Nó giống như việc phân tích ngữ pháp một câu văn: đâu là chủ ngữ, vị ngữ, động từ... để máy tính có thể hiểu cấu trúc logic của code.
Ví dụ, dòng code const total = price * quantity không chỉ là một chuỗi ký tự. Khi parse thành AST, engine sẽ nhìn thấy: đây là một khai báo biến, tên biến là total, giá trị của nó là một phép nhân giữa price và quantity.
AST rút gọn
Từ const total = price * quantity thành một cái cây mà engine có thể đi từng nhánh để hiểu code.
id
Identifier(total)
Tên biến là total
init
BinaryExpression(*)
Giá trị khởi tạo là một phép nhân
left
Identifier(price)
right
Identifier(quantity)
Giai đoạn 3: JIT (Just-In-Time compile) → Machine Code
Đây là bước quyết định tốc độ của JS (đặc biệt là engine V8 trong Chrome): JIT (Biên dịch tức thời) - trình duyệt vừa chạy vừa biên dịch code thành mã máy (machine code) để CPU có thể thực thi trực tiếp. JIT giúp JS nhanh hơn rất nhiều so với thông dịch thuần túy. Đối với những đoạn code được gọi liên tục, tốc độ có thể tiếp cận các ngôn ngữ biên dịch.
Giai đoạn 4: Change DOM/CSSOM
Khi code JS chạy, nó thường làm những việc như: thêm/xóa phần tử HTML (document.createElement), thay đổi class, style (element.style.width). Đây chính là hành động can thiệp vào DOM Tree và CSSOM đã được xây dựng trước đó.
Giai đoạn 5: Reflow/Repaint
Đây là bước quan trọng nhất về mặt hiệu năng. Vì JS đã thay đổi cấu trúc (DOM) hoặc giao diện (CSSOM), trình duyệt bắt buộc phải tính toán lại và vẽ lại.
Reflow: Nếu JS đổi kích thước (width, height), vị trí... trình duyệt phải chạy lại bước Layout (sắp xếp lại trang).
Repaint: Nếu JS đổi màu sắc (color, background), trình duyệt phải chạy lại bước Paint.
Đây chính xác là lý do React tồn tại! Thay vì thao tác trực tiếp trên DOM (gây Reflow/Repaint liên tục), React dùng Virtual DOM để tính toán sự khác biệt trước, rồi chỉ cập nhật những phần thực sự thay đổi - giảm thiểu Reflow/Repaint tối đa.
DNS làm nhiệm vụ gì?
Dịch tên miền (sydexa.com) thành địa chỉ IP (93.184.216.34) - "danh bạ điện thoại của Internet".
Từ URL → DNS → Rendering Pipeline → JavaScript blocking, tất cả đều xoay quanh một câu hỏi: trình duyệt biến code thành pixel như thế nào? Nhưng có một thành phần quan trọng ta mới chỉ chạm bề mặt - JavaScript. Nó chặn rendering, nó thay đổi DOM, nó điều khiển mọi tương tác. React - thứ bạn sắp học - được xây hoàn toàn bằng JavaScript. Và để hiểu React thật sự, trước tiên phải hiểu sâu những khái niệm JavaScript nền tảng mà React dựa vào.
Critical Rendering Path (CRP) là chuỗi bước tối thiểu để biến HTML/CSS/JS thành pixel đầu tiên trên màn hình. Mục tiêu của tối ưu frontend là rút ngắn chuỗi này để cải thiện First Contentful Paint (FCP) và cảm nhận tốc độ.
Chuỗi CRP có thể hình dung như một dây chuyền: browser đọc HTML để tạo DOM, đọc CSS để tạo CSSOM, ghép DOM + CSSOM thành Render Tree, rồi mới tính Layout và Paint ra pixel. Nếu mắt xích nào trong chuỗi này bị chậm, frame đầu tiên cũng bị chậm theo.
Ví dụ: trang có hero ở màn đầu. HTML cho browser biết hero tồn tại, CSS cho biết hero nằm ở đâu và trông như thế nào, còn script sync trong <head> có thể bắt HTML parser dừng lại giữa đường. Vì vậy CRP không chỉ là “tải xong file”, mà là “đủ DOM, CSSOM và layout để browser vẽ được pixel đầu tiên”.
Khi nhìn vào chuỗi này, câu hỏi tiếp theo không chỉ là browser phải làm những bước nào, mà là bước nào bắt buộc phải xong trước khi browser được vẽ frame đầu tiên. Có tài nguyên browser có thể tải song song rồi paint trước; có tài nguyên lại khiến browser phải chờ. Vì vậy, ta cần phân biệt tài nguyên nào thật sự chặn CRP.
Trong React apps (Vite/Next.js/CRA), bundle JavaScript thường lớn hơn app đa phần static. Kết hợp code splitting, lazy loading và tối ưu CSS critical giúp giảm thời gian đến first paint rõ rệt, đặc biệt trên mạng 3G/4G yếu.
Sau khi DOM/CSSOM có sẵn, browser đi qua các bước: Style → Layout → Paint → Composite. Không phải thay đổi nào cũng tốn tài nguyên như nhau.
| Giai đoạn | Browser làm gì | Chi phí thường gặp |
|---|---|---|
| Style Recalculation | Tính lại CSS áp dụng cho element | Tăng khi selector phức tạp hoặc thay đổi class trên cây DOM lớn |
| Layout (Reflow) | Tính lại kích thước và vị trí phần tử | Đắt vì có thể lan truyền sang các node liên quan |
| Paint (Repaint) | Vẽ lại màu sắc, border, shadow, text | Đắt khi vùng vẽ lớn hoặc hiệu ứng visual phức tạp |
| Composite | Ghép các layer đã paint thành frame cuối | Thường rẻ hơn layout/paint, đặc biệt khi chỉ thay đổi transform/opacity |
Quy tắc thực chiến: ưu tiên animation/transitions trên transform và opacity vì trong đa số engines hiện đại, chúng thường chỉ cần tổng hợp lại (composite). Ngược lại, thay đổi width, height, top, left dễ kéo theo layout + paint.
| Property thay đổi | Ảnh hưởng chính | Ghi chú thực hành |
|---|---|---|
| width / height / top / left | Thường trigger Layout + Paint + Composite | Tránh animate liên tục ở 60fps nếu không cần thiết |
| background-color / color | Thường trigger Paint + Composite | Ít tốn hơn layout nhưng vẫn ảnh hưởng nếu repaint vùng lớn |
| transform / opacity | Thường chỉ Composite (đa số engine hiện đại) | Lựa chọn tốt cho animation mượt |
Layout thrashing xảy ra khi code JS liên tục xen kẽ thao tác đọc layout và ghi layout trong cùng frame, khiến browser phải forced synchronous layout nhiều lần.
React có pipeline riêng: Render phase (reconciliation) và Commit phase (ghi thay đổi xuống DOM). Browser cũng có pipeline riêng: Style → Layout → Paint → Composite.
Nghĩa là: tối ưu React re-render chưa đủ, bạn còn cần tối ưu cả chi phí browser rendering sau commit. Ví dụ: component re-render ít nhưng mỗi commit lại đổi width của hàng trăm node thì vẫn có thể lag do layout work lớn.
Các kỹ thuật như React.memo, useMemo, useCallback giúp giảm số lần re-render không cần thiết. Nhưng để animation mượt, bạn vẫn nên chọn CSS properties phù hợp (ưu tiên transform/opacity) và tránh thao tác DOM gây layout thrashing trong effect/event handler.
// Absolute URL - dùng khi gọi API cross-origin
fetch('https://api.sydexa.com/users');
// Root-relative - dùng nhiều với React Router (cùng origin)
<Link to="/blog/reactjs" />
// Path-relative - resolve từ URL hiện tại
// Nếu đang ở /blog, "./reactjs" -> /blog/reactjs
// Nếu đang ở /blog/posts, "./reactjs" -> /blog/posts/reactjs
<Link to="./reactjs" /><head>
<!-- 1. CSS phải tải và parse để tạo CSSOM -->
<link rel="stylesheet" href="/styles.css" />
<!-- 2. Script sync làm HTML parser dừng trước khi DOM hoàn thiện -->
<script src="/analytics-or-app.js"></script>
</head>
<body>
<!-- 3. DOM + CSSOM -> Render Tree -> Layout -> Paint -->
<section class="hero">
<h1>Khóa học React Advanced</h1>
<img src="/hero.png" alt="React course hero" />
</section>
</body>// encodeURIComponent - encode GIÁ TRỊ của từng tham số
const keyword = 'React hướng dẫn & tips';
const encoded = encodeURIComponent(keyword);
// -> "React%20h%C6%B0%E1%BB%9Bng%20d%E1%BA%ABn%20%26%20tips"
// Dùng trong URL
const url = `https://sydexa.com/search?q=${encoded}`;
// -> https://sydexa.com/search?q=React%20h%C6%B0%E1%BB%9Bng%20d%E1%BA%ABn%20%26%20tips
// SAI: không encode sẽ làm hỏng URL vì & là dấu phân cách query
// https://sydexa.com/search?q=React hướng dẫn & tips
// decodeURIComponent - giải mã
console.log(decodeURIComponent(encoded));
// -> "React hướng dẫn & tips"
// URLSearchParams - API hiện đại, tự encode
const params = new URLSearchParams({ q: keyword, lang: 'vi' });
console.log(params.toString());
// -> "q=React+h%C6%B0%E1%BB%9Bng+d%E1%BA%ABn+%26+tips&lang=vi"const url = new URL('https://sydexa.com/courses?tag=react&tag=js&page=2&active=true');
// Lấy một giá trị
url.searchParams.get('tag'); // "react" (đầu tiên)
url.searchParams.get('page'); // "2" (string, không phải number!)
url.searchParams.get('active'); // "true" (string, không phải boolean!)
// Lấy tất cả giá trị của key lặp
url.searchParams.getAll('tag'); // ["react", "js"]
// Parse kiểu dữ liệu thủ công
const page = Number(url.searchParams.get('page')); // 2
const isActive = url.searchParams.get('active') === 'true'; // true<!-- dns-prefetch: Chỉ thực hiện DNS lookup trước -->
<!-- Dùng khi bạn biết sẽ cần tài nguyên từ domain này nhưng chưa rõ resource cụ thể -->
<link rel="dns-prefetch" href="https://cdn.example.com" />
<!-- preconnect: DNS lookup + TCP handshake + TLS negotiation -->
<!-- Dùng khi bạn chắc chắn sẽ tải tài nguyên từ domain này ngay -->
<!-- Chi phí cao hơn dns-prefetch, nên chỉ dùng cho 2-3 domain quan trọng nhất -->
<link rel="preconnect" href="https://api.example.com" /><!-- Ưu tiên CSS quan trọng -->
<link rel="preload" href="/styles/critical.css" as="style" />
<link rel="stylesheet" href="/styles/critical.css" />
<!-- Script không chặn parser -->
<script defer src="/assets/app.js"></script>
<!-- Với script độc lập, không phụ thuộc thứ tự -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"></script>// Anti-pattern: read -> write -> read -> write trong loop
for (const item of items) {
const width = item.offsetWidth; // read layout
item.style.width = width + 10 + 'px'; // write layout
}
// Tốt hơn: batch reads trước, rồi batch writes
const widths = items.map((item) => item.offsetWidth); // read batch
requestAnimationFrame(() => {
items.forEach((item, i) => {
item.style.width = widths[i] + 10 + 'px'; // write batch
});
});