SydexaSydexa

React Nâng Cao

Kiến thức cơ bảnJavaScript cho React
TypeScript Cơ BảnType System & GenericUtility Types & tsconfig
ReactJS Căn Bản
Reconciliation & FiberFiber Deep Dive
React HooksOptimization & Utility
React Hook Form & ZodAdvanced Form Patterns
Web Vitals & DevToolsPerformance Playbooks
TanStack VirtualTanStack Virtual (P2)
TanStack QueryTanStack Query (P2)
ZustandZustand (P2)Fiber ↔ External Store
Bảo mật frontendBảo mật frontend (2)
Trang chủKhoá họcReact Nâng Cao
Những kiến thức cơ bản - Bước đệm cho ReactJS
Từ URL, DNS đến Rendering pipeline - hiểu cách trình duyệt biến code thành pixel trên màn hình. Nền tảng web mà mọi React developer cần nắm vững.

Warm-up Quiz

1.React dùng const [count, setCount] = useState(0) - bạn có biết cú pháp ngoặc vuông [count, setCount] đó là kỹ thuật gì của JavaScript thuần không?
1/1
1/1

URL (Uniform Resource Locator)

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:

URL Anatomy: bóc tách từng phần

https://api.sydexa.vn:80/users/search?name=Minh&role=admin#results

Schemehttps://Nằm trước dấu ://

Giao thức trình duyệt dùng để nói chuyện với server. HTTPS là HTTP có lớp mã hóa bảo mật.

Phân tích URL

1.Cho URL: https://api.sydexa.vn:8080/users/search?name=Minh&role=admin#results. Scheme (giao thức) của URL trên là gì?
1/5
1/5

Ở 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.

Absolute vs Relative URL

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 URLVí dụKhi nào dùng
Absolutehttps://api.sydexa.com/usersGọi API ngoài, linh hoạt khi deploy nhiều môi trường
Root-relative/users/searchLink 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 Encoding/Decoding

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.

Query Params nâng cao

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.

  • Multi-value params - Một key có thể lặp nhiều lần: ?tag=react&tag=js. Dùng URLSearchParams.getAll("tag") để lấy tất cả.
  • Kiểu dữ liệu luôn là string - ?page=2 không phải number mà là string "2". ?active=true là string "true", không phải boolean. Cần parse thủ công.

Origin và CORS

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 AURL BSame-origin?Lý do
https://sydexa.com/bloghttps://sydexa.com/apiCóCùng protocol, host, port (443)
https://sydexa.comhttp://sydexa.comKhôngKhác protocol (https vs http)
https://sydexa.comhttps://api.sydexa.comKhôngKhác host (subdomain khác = host khác)
http://localhost:3000http://localhost:8080KhôngKhá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.

  • Same-origin request - Browser cho phép tự do, không cần header CORS đặc biệt.
  • Cross-origin request - Browser gửi request nhưng sẽ chặn kết quả nếu server không trả đúng CORS headers. Đây là lỗi thường gặp nhất khi bắt đầu làm React.

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.

URL Practical Quiz

1.Cho URL: https://sydexa.com/search?q=React%20hooks%20%26%20tips. Giá trị thực của tham số q sau khi decode là gì?
1/3
1/3

DNS (Domain Name System)

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ố.

0:00/0:00

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.

DNS Quiz

1.Khi bạn truy cập một trang web lần đầu và DNS Cache trống, thứ tự truy vấn DNS đệ quy là gì?
1/1
1/1

DNS Resolution: Hành trình từ tên miền đến địa chỉ IP

Ở 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.

Chuỗi tra cứu DNS (DNS Lookup Chain)

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:

  • Browser DNS Cache - Trình duyệt lưu kết quả DNS gần đây. Nếu đã truy cập trang này trước đó và cache chưa hết hạn (TTL), trình duyệt dùng luôn IP từ cache.
  • OS DNS Cache - Nếu browser cache trống, hệ điều hành kiểm tra cache riêng. Trên Windows bạn có thể xem bằng lệnh ipconfig /displaydns.
  • Recursive Resolver (ISP/Public DNS) - Nếu cả browser và OS đều không có, yêu cầu được gửi đến recursive resolver - thường là DNS server của ISP hoặc public DNS như 8.8.8.8 (Google), 1.1.1.1 (Cloudflare).

Recursive Resolver làm gì?

Đâ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:

  • 1. Root Name Server - Resolver hỏi Root Server: "IP của sydexa.com là gì?". Root Server không biết IP, nhưng trả về referral (giới thiệu) đến TLD nameserver quản lý .com.
  • 2. TLD Name Server (.com) - Resolver hỏi tiếp TLD Server .com. TLD Server cũng không biết IP cuối cùng, nhưng trả về referral đến authoritative nameserver của sydexa.com.
  • 3. Authoritative Name Server - Resolver hỏi authoritative server - đây là server có thẩm quyền cuối cùng cho domain sydexa.com. Nó trả về IP chính xác (ví dụ: 93.184.216.34).

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.

Lưu ý:

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ó.

Các loại DNS Record quan trọng

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:

RecordMục đíchVí dụ giá trị
AÁnh xạ domain → IPv4 addresssydexa.com → 93.184.216.34
AAAAÁnh xạ domain → IPv6 addresssydexa.com → 2606:2800:220:1:248:1893:25c8:1946
CNAMEAlias - trỏ domain này sang domain khácwww.sydexa.com → sydexa.com
NSChỉ định nameserver có thẩm quyền cho domainsydexa.com → ns1.cloudflare.com
MXChỉ định mail server nhận email cho domainsydexa.com → mail.google.com (priority 10)
TXTLưu text tùy ý - thường dùng cho xác minh domain, SPF, DKIMv=spf1 include:_spf.google.com ~all
CNAME và hạn chế:

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).

TTL (Time To Live) và DNS Propagation

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.

  • TTL ngắn (vd: 60 giây) - Thay đổi DNS được cập nhật nhanh, nhưng tăng số lượng truy vấn đến authoritative server.
  • TTL dài (vd: 86400 giây = 24 giờ) - Giảm tải cho DNS server, nhưng khi đổi IP, phải chờ lâu hơn để mọi nơi cập nhật.

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 và hiệu năng web

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ậtThực hiệnKhi nào dùng
dns-prefetchChỉ resolve DNSDomain sẽ dùng sau (không urgent), hoặc khi có nhiều domain third-party
preconnectDNS + TCP + TLSDomain 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 và bảo mật (awareness level)

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:

  • DNS Spoofing / Cache Poisoning - Kẻ tấn công đưa bản ghi DNS giả vào cache của resolver, khiến người dùng truy cập domain thật nhưng bị chuyển đến IP giả mạo. Đây là lý do không nên dùng DNS resolver không tin cậy.
  • DNS over HTTPS (DoH) - Mã hóa truy vấn DNS trong HTTPS, ngăn ISP hoặc kẻ trung gian theo dõi hoặc can thiệp DNS. Firefox và Chrome đều hỗ trợ.
  • DNS over TLS (DoT) - Tương tự DoH nhưng sử dụng TLS trực tiếp trên port 853. Phổ biến ở cấp hệ điều hành và router hơn là trình duyệt.

DNS Deep Dive Quiz

1.Khi recursive resolver nhận yêu cầu từ client, nó thực hiện truy vấn đến các DNS server theo kiểu nào?
1/4
1/4

Chuỗi kiểm tra DNS cache trước khi tra cứu bên ngoài?

Nhấp vào thẻ để lật

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.

Nhấp vào thẻ để lật lại
1/5

Webpage Rendering - Từ code đến pixel

0:00/0:00

JavaScript chặn rendering như thế nào?

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.

VariableDeclarationKhai báo biến
VariableDeclaratorMột biến cụ thể trong câu lệnh

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.

Rendering Quiz

1.Phần tử nào sau đây KHÔNG nằm trong Render Tree?
1/2
1/2

DNS làm nhiệm vụ gì?

Nhấp vào thẻ để lật

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".

Nhấp vào thẻ để lật lại
1/10
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) trong thực tế

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.

Tài nguyên nào chặn CRP?

  • CSS là render-blocking - Trình duyệt cần CSSOM để tính style chính xác trước khi render. CSS lớn hoặc tải chậm làm chậm First Paint.
  • Synchronous JavaScript có thể parser-blocking - Khi parser gặp <script> không có defer/async, nó dừng parse HTML để tải và thực thi script.
  • Web font và hero banner ảnh hưởng trải nghiệm - Không phải là render-blocking theo định nghĩa kỹ thuật như CSS hay synchronous JS - browser vẫn có thể tiếp tục parse HTML và paint trang mà không cần đợi chúng tải xong, nhưng có thể làm người dùng thấy trang trống/lệch layout ở giai đoạn đầu.

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.

Chi phí của từng giai đoạn rendering

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ạnBrowser làm gìChi phí thường gặp
Style RecalculationTính lại CSS áp dụng cho elementTă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
CompositeGhép các layer đã paint thành frame cuốiThường rẻ hơn layout/paint, đặc biệt khi chỉ thay đổi transform/opacity

Property cost awareness: đổi property nào thì tốn bước nào?

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ínhGhi chú thực hành
width / height / top / leftThường trigger Layout + Paint + CompositeTránh animate liên tục ở 60fps nếu không cần thiết
background-color / colorThườ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 / opacityThường chỉ Composite (đa số engine hiện đại)Lựa chọn tốt cho animation mượt

Layout Thrashing: anti-pattern phổ biến

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 render vs Browser render: hai pipeline khác nhau

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.

Rendering Performance Quiz

1.Bạn muốn animate panel trượt từ trái sang phải mượt nhất. Lựa chọn nào thường tối ưu hơn?
1/4
1/4
Bài tiếp theo
url-examples.tsx
tsx
12345678910
// 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" />
critical-rendering-path.html
html
1234567891011121314
<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>
url-encoding.ts
ts
1234567891011121314151617181920
// 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"
query-params.ts
ts
12345678910111213
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
index.html
html
12345678
<!-- 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" />
index.html
html
123456789
<!-- Ư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>
layout-thrashing-example.ts
ts
1234567891011121314
// 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
  });
});