Cookie là công cụ quen thuộc để giữ phiên đăng nhập và trạng thái người dùng. Nhưng có một cái giá ẩn ít khi được đo: mỗi cookie bạn đặt sẽ được gửi lại kèm mọi request tới miền đó — không chỉ những request cần nó, mà cả request tải ảnh, CSS, hay gọi API. Bài này dựng một máy chủ đặt cookie của riêng tôi, đo chính xác số byte cookie cộng vào từng request, và chỉ ra vì sao con số nhỏ trên một request lại trở thành gánh nặng thật khi nhân lên.
Cookie: đặt một lần, gửi lại mỗi request
Cơ chế đơn giản. Server gửi header Set-Cookie một lần, trình duyệt lưu lại. Từ đó về sau, mọi request tới miền khớp đều tự động kèm một header Cookie chứa các cookie đã lưu — bạn không phải làm gì, trình duyệt tự đính. Nhờ vậy server nhận ra bạn giữa các request mà không cần trạng thái phía nó.
Phạm vi gửi được quyết định bởi hai thuộc tính: Domain (miền nào) và Path (đường dẫn nào). Một cookie đặt Path=/ đi tới mọi đường dẫn của miền; một cookie Path=/admin chỉ đi tới /admin/*. Path chính là cái quyết định request nào phải cõng cookie đó — một chi tiết sẽ trở nên quan trọng.
Đo: 147 byte cộng vào mỗi request
Tôi cho server đặt một bộ cookie khá thực tế: một sid phiên đăng nhập, hai cookie theo dõi kiểu Google Analytics (_ga, _gid), một cookie tùy chọn giao diện (prefs) — tất cả Path=/ — và một admincsrf token dài, Path=/admin. Rồi tôi đo kích thước request tới nhiều đường dẫn khác nhau:
| Request | Kích thước | Cookie kèm theo |
|---|---|---|
| (không cookie) | 63 B | — |
| GET / | 211 B | 147 B (4 cookie) |
| GET /style.css | 220 B | 147 B (4 cookie) |
| GET /img/logo.png | 223 B | 147 B (4 cookie) |
| GET /admin/panel | 292 B | 217 B (5 cookie) |
Đọc bảng này có hai điều đáng chú ý. Thứ nhất, cookie admincsrf với Path=/admin chỉ xuất hiện ở request /admin/panel, không có ở các đường dẫn khác — Path scoping hoạt động, và nó là công cụ để giới hạn cookie chỉ tới nơi cần. Thứ hai, và quan trọng hơn: đúng 147 byte cookie đó nằm trong cả request cho /style.css và /img/logo.png — một tệp CSS và một ảnh logo chẳng dùng gì tới phiên đăng nhập hay cookie theo dõi, nhưng vẫn phải cõng chúng.
Một lần tôi đo hớ: 147 byte trông nhỏ, cho tới khi nhân lên
Nhìn con số 147 byte cộng vào một request, phản ứng đầu tiên của tôi là bỏ qua: "cookie nhỏ xíu, so với nội dung trang thì chẳng đáng lo". Đó là lúc tôi mới đo một request.
Nhưng rồi tôi để ý đúng 147 byte đó lặp lại ở mọi dòng trong bảng — request cho CSS, cho ảnh, cho API. Cookie không được gửi "tới những request cần nó", mà tới mọi request khớp Domain và Path. Một trang web điển hình tải 30 tới 50 tài nguyên từ cùng một miền, nên cái giá thật không phải 147 byte một lần mà là 147 byte nhân số request:
147 byte × 30 request = ~4,4 KB tải lên chỉ riêng cookie
Và kho cookie của tôi còn khá khiêm tốn. Một trang thật với vài cookie theo dõi, token phiên dài, và các cookie tùy chọn dễ dàng có 1 tới 4 KB cookie — nhân với 30-50 request thành hàng trăm KB tải lên cho mỗi lần xem trang, phần lớn trên những request (ảnh, CSS, gọi API nền) chẳng bao giờ đọc tới cookie. Trên mạng di động nơi băng thông lên thường hẹp hơn băng thông xuống nhiều lần, khoản tải lên thừa này làm chậm thấy rõ.
Bài học đo lường: chi phí của một thứ lặp lại phải được nhân với tần suất, đừng chỉ đo một lần. 147 byte là con số vô hại nếu xảy ra một lần; nhân với mọi request tới miền, nó thành một khoản thuế cố định đè lên toàn bộ tài nguyên tĩnh — đúng những thứ lẽ ra phải nhẹ và cache được. Tôi suýt kết luận "cookie không đáng lo về kích thước" chỉ vì nhìn một mẫu thay vì cả phân bố request thật.
Khi cookie phình quá to
Kích thước cookie không chỉ là chuyện băng thông — nó có một trần cứng. Trình duyệt giới hạn khoảng 4 KB cho mỗi cookie và một số lượng cookie nhất định cho mỗi miền; vượt quá thì cookie mới bị âm thầm loại bỏ. Và ở phía server, tổng khối header (mà cookie là phần lớn) cũng có trần: nhiều máy chủ đặt giới hạn header khoảng 8 tới 16 KB, vượt quá thì trả về 431 Request Header Fields Too Large hoặc 400 Bad Request — request bị từ chối trước cả khi tới code ứng dụng.
Đây là một sự cố production khó chịu và khó lần: người dùng tích lũy quá nhiều cookie (thường từ nhiều dịch vụ theo dõi trên cùng miền), tổng vượt trần header, và đột nhiên mọi request của họ bị máy chủ từ chối — trang chết hẳn với một người dùng cụ thể mà log ứng dụng không thấy gì, vì request bị chặn ở tầng máy chủ. Triệu chứng "sửa được bằng cách xóa cookie" gần như luôn là dấu hiệu của chuyện này. Giữ cookie nhỏ không chỉ để nhanh, mà để không tự dựng một quả bom hẹn giờ theo từng người dùng.
Vì sao điều này quan trọng khi lập trình
Hệ quả đầu tiên, và là một tối ưu kinh điển: phục vụ tài nguyên tĩnh từ một miền không cookie. Nếu ảnh, CSS, JS được phục vụ từ static.example.com (hoặc một CDN) khác với miền chính đặt cookie, thì các request tài nguyên đó không mang theo cookie nào — cắt sạch khoản thuế 147 byte (hay vài KB) trên mỗi tài nguyên. Đây là lý do lịch sử các trang lớn dùng miền riêng cho tài nguyên tĩnh.
Hệ quả thứ hai là giới hạn phạm vi cookie chặt tay. Đặt Path đúng cho những cookie chỉ cần ở một khu vực (như admincsrf chỉ cho /admin) để chúng không rò ra mọi request. Và đừng đặt cookie cho những dữ liệu không cần gửi lên server mỗi lần — trạng thái chỉ dùng ở phía trình duyệt nên nằm trong localStorage chứ không phải cookie, vì cookie luôn đi kèm request còn localStorage thì không. Một token xác thực chỉ cần cho vài lời gọi API có thể gửi qua header Authorization đúng chỗ, thay vì thành cookie đi theo mọi thứ.
Hệ quả thứ ba nối với các phần trước: trong HTTP/1.1, khối cookie được gửi không nén và lặp lại nguyên vẹn mỗi request, đúng như phần header đã đo ở bài một GET. HTTP/2 với HPACK nén được phần header lặp lại (gồm cookie không đổi), nên nâng lên HTTP/2 giảm bớt cái giá này — nhưng không xóa hẳn, và nó không giúp gì cho lần request đầu tiên. Con số mang theo: mỗi cookie bạn đặt là một khoản byte cộng vào mọi request tới miền, kể cả những request không dùng nó — cái giá thật bằng kích thước cookie nhân số request, không phải một lần. Cookie tiện, nhưng mỗi cookie là một cam kết băng thông trên từng request về sau; đặt ít, scope chặt, và để tài nguyên tĩnh ở nơi không cookie.
Thử ba mươi giây
Mở DevTools tab Network trên một trang bạn đã đăng nhập, chọn một request tải ảnh hoặc CSS, và xem mục Request Headers: bạn sẽ thấy một header Cookie dài — chính những cookie phiên và theo dõi đang được gửi kèm một tấm ảnh chẳng cần chúng. Cộng độ dài header Cookie đó lại rồi nhân với số request trong tab Network để ước lượng tổng byte cookie tải lên cho một lần xem trang. Con số đó thường lớn đến bất ngờ, và gần như toàn bộ nằm trên những request không đọc tới cookie.