Tối ưu tốc độ website Next.js

Những gì Perxel đã làm để website Next.js nhiều animation của mình tải nhanh, chia theo nhóm: cách đo, giao diện, code, tài nguyên, script bên thứ ba và máy chủ Cloudflare Workers. Mỗi mục có số đo trước và sau, kèm điều kiện đo.

Phúc Bùi · 10 tháng 10, 2026

Next.js có sẵn nhiều thứ giúp website tải nhanh: prerender trang tĩnh, tách JavaScript theo route, tối ưu ảnh và font. Tuy vậy, một website nhiều animation vẫn có thể chậm nếu không để ý từng phần, và khi chạy trên Cloudflare Workers thay vì Vercel thì có thêm những việc Next.js không tự làm.

Bài viết này tổng hợp những gì Perxel đã làm để tăng tốc website của chính mình, chia theo nhóm: cách đo, giao diện, code, tài nguyên, script bên thứ ba và máy chủ. Mỗi mục có số đo trước và sau, kèm điều kiện đo. Website dùng Next.js 16, React 19, GSAP và nhiều canvas, chạy trên Cloudflare Workers qua OpenNext. Phần lớn các bước áp dụng được cho mọi dự án Next.js. Riêng phần máy chủ dành cho trường hợp chạy trên Cloudflare.

Đo trước khi sửa

Điểm PageSpeed thay đổi theo từng lần đo, nên một con số đơn lẻ không đủ để biết một thay đổi có hiệu quả hay không. Hai lần đo dưới đây chạy trên cùng một phiên bản code của trang chủ, cách nhau hơn 13 tiếng: lần đầu được 68 điểm, lần sau được 95.

Hai báo cáo PageSpeed Insights bản mobile của trang chủ perxel.com trên cùng một phiên bản code: 68 điểm với LCP 6,9 giây, và 95 điểm với LCP 2,3 giây

Cùng code, hai lần đo PageSpeed bản mobile: 68 điểm lúc 20:12 ngày 9/10/2026 và 95 điểm lúc 9:36 ngày 10/10/2026.

Vì vậy Perxel đo theo vài nguyên tắc:

  • Chạy Lighthouse trên máy mình, cùng phiên bản với PageSpeed. PageSpeed Insights hiện dùng Lighthouse 13.5 với throttling giả lập. Chạy cùng phiên bản trên máy mình cho kết quả gần với PageSpeed và không bị giới hạn số lần gọi API.
  • Lấy trung vị của nhiều lần đo. Ngày 10/10/2026, ba lần đo bản mobile trên production cho 86, 91 và 93 điểm, hai lần đo bản desktop cho 96 điểm. Khi so sánh hai phiên bản, Perxel chạy xen kẽ từng cặp để máy đo chịu cùng điều kiện.
  • Kiểm tra lại LCP bằng throttling thật khi con số đáng ngờ. Chế độ giả lập cộng thời gian tải của mọi request bắt đầu trước LCP vào chính LCP. Có lần chế độ giả lập báo LCP 4,6 giây trong khi throttling thật (--throttling-method=devtools) đo được 1,8 giây.
  • Xem cấu hình máy đo trước khi nghi ngờ code. Có lần PageSpeed cho 47 và 58 điểm, trong khi Lighthouse cùng phiên bản trên máy Perxel cho 83 đến 98 điểm với cùng trang. TBT cao gấp 5 đến 10 lần thường là dấu hiệu máy đo đang chậm hoặc bận. Thông số này nằm ở mục "CPU/Memory Power" cuối báo cáo.
  • Xem layout-shifts trước khi xem điểm CLS. Một lần xê dịch có thể chiếm toàn bộ điểm CLS. Lighthouse với --save-assets giữ lại trace, trong đó có vị trí của từng phần tử bị xê dịch.
  • Muốn biết chuyện gì xảy ra lúc tải trang thì gắn script theo dõi vào trang. Perxel điều khiển Chrome headless qua DevTools Protocol, gắn một script chạy trước mọi thứ để ghi lại animationstart, thay đổi DOM và các lần startViewTransition. Lỗi view transition ở phần Code bên dưới được tìm ra theo cách này, vì điểm số và CLS đều không cho thấy nó.

Điểm lab và dữ liệu người dùng thật (CrUX) khác nhau thế nào được giải thích trong bài PageSpeed Insights & Core Web Vitals.

Giao diện: thứ người xem thấy đầu tiên phải hiện sớm

Phần tử LCP không chờ JavaScript

Tiêu đề lớn ở trang chủ là phần tử LCP. Bản đầu tiên cho tiêu đề xuất hiện bằng GSAP, nên tiêu đề phải chờ JavaScript tải và chạy xong mới hiện: LCP bản mobile lên tới 8,7 giây. Bây giờ phần co giãn chữ theo khung dùng CSS (đơn vị cqw trên chiều rộng dòng tính bằng em), animation xuất hiện là CSS keyframes, và một đoạn script inline nhỏ chỉ hiện khối chữ khi font tiêu đề đã tải xong (chờ tối đa 1,2 giây). LCP giảm còn khoảng 1,8 giây và bằng FCP.

Có hai chi tiết dễ vấp. Thứ nhất, các chữ bắt đầu ở opacity 1% thay vì 0, vì Chrome không tính một phần tử có opacity 0 lúc hiện lần đầu là ứng viên LCP. Thứ hai, đoạn script inline nằm cuối khối chữ: trình duyệt có thể vẽ trang trong lúc chờ script, nên mọi thứ đặt sau nó sẽ hiện muộn và làm tiêu đề ở giữa màn hình bị xê dịch.

Tiêu đề hiện ngay khi tải trang, nhân vật Lottie vào sau.

Trạng thái ban đầu nằm sẵn trong CSS

HTML từ server được vẽ lên trước khi React hydrate. Vì vậy, nếu một useLayoutEffect thay đổi layout sau đó, người xem vẫn thấy trang bị xê dịch. Ở trang chủ Perxel, phần hero kéo dải màu xanh bên dưới lên một màn hình bằng margin-top để làm hiệu ứng nước dâng. Lúc đầu thao tác này chạy bằng JavaScript, nên CLS bản mobile trên production là 0,9 và desktop là 0,64. Sau khi đưa giá trị mặc định vào CSS (JavaScript chỉ ghi đè sau đó), CLS về 0.

Quy tắc rút ra: phần layout do JavaScript thay đổi cần có sẵn trạng thái ban đầu trong CSS. Khi đổi kích thước của phần đó, phải đổi cả trong CSS.

Đơn giản hóa giao diện trên điện thoại

Trên điện thoại, Perxel giữ nguyên nội dung nhưng thay vài hiệu ứng nặng bằng phiên bản nhẹ hơn:

  • Nhân vật ở hero có ảnh poster là SVG inline. Animation Lottie cần tải 486 KB WASM, nên trên màn hình dưới 768px, chỗ của nhân vật hiện sẵn frame đầu tiên dưới dạng SVG (22 KB, 9,6 KB khi gzip) rồi chuyển sang Lottie khi đã sẵn sàng. Bản đầu dùng ảnh WebP thì ảnh này trở thành phần tử LCP thay cho tiêu đề. SVG inline thì không được tính là ứng viên LCP.
  • Mép nước trên màn hình cảm ứng là CSS, không phải canvas. Trên desktop, mép nước là canvas phản ứng với con trỏ. Trên điện thoại không có con trỏ nên Perxel thay bằng một mask hình sóng trượt ngang bằng transform, không có script chạy mỗi frame.
  • Dải logo công nghệ có chế độ nhẹ trên điện thoại. Canvas mép nước vẽ ở mật độ điểm ảnh 1x, các viên logo dùng viền phẳng 1px thay cho bóng đổ mờ 18px, và phần mô phỏng vật lý chạy cách một frame.
  • Màn hình dọc nhận danh sách dự án thường. Hàng card uốn theo sóng bằng canvas chỉ chạy trên màn hình ngang cao từ 600px. Điện thoại và tablet dựng đứng nhận danh sách dọc.
  • Pin màn hình dùng position: sticky thay cho pin: true của GSAP. Pin của GSAP chuyển phần tử sang position: fixed và di chuyển nó bằng JavaScript, nên trên Safari iOS phần này trễ hơn thao tác cuộn và bị giật. Với sticky, trình duyệt tự giữ phần tử, ScrollTrigger chỉ còn điều khiển animation theo vị trí cuộn.

Phần nặng ở phía dưới khởi tạo muộn

Các section dùng canvas ở cuối trang vẫn dựng pin và layout ngay lúc đầu, vì layout không được thay đổi về sau. Riêng phần vẽ và tải dữ liệu được hoãn lại. Hàng card dự án chỉ tải ảnh và video khi người dùng cuộn tới gần (trong khoảng 2 lần chiều cao màn hình). Phần chữ rót nước và con cá voi (font, bố cục chữ, WebGL) khởi tạo 1,5 giây sau khi trang tải xong, hoặc sớm hơn nếu người dùng cuộn, chạm hay bấm phím. Trước đó, video 1,2 MB của một dự án được tải ngay khi mở trang dù nằm cách màn hình đầu vài lần cuộn.

Số đo trước và sau: LCP bản mobile từ 8,7 giây còn 1,8 giây, CLS bản mobile từ 0,9 về 0, dữ liệu tải lúc mở trang từ 2.736 KB còn 1.337 KB

Trang chủ bản mobile. LCP đo bằng Lighthouse với throttling thật, CLS trên PageSpeed production, dữ liệu tải lúc mở trang đo bằng Chrome headless với CPU chậm 4 lần (tháng 10/2026).

Code: mỗi trang chỉ tải JavaScript nó cần

Server component vẫn có thể kéo client component vào trang

Next.js đưa vào script của một trang mọi client component nằm trong cây module của server component, kể cả component không được render. Footer của website Perxel đọc danh sách bài viết trên mọi trang, và danh sách này lấy frontmatter bằng cách import file MDX. Vì vậy mọi client component mà một bài viết nào đó dùng (dialog, accordion, lightbox, Lottie player) đều có mặt trong JavaScript của mọi trang, chiếm 5 đến 18% JavaScript mỗi trang.

Cách sửa là đọc frontmatter từ JSON thay vì từ MDX. Một script chạy trước next dev và next build ghi frontmatter của mọi bài vào content/meta.generated.json, và chỉ route chi tiết của từng bài mới import file MDX. Đây là thay đổi giảm JavaScript nhiều nhất. Để kiểm tra một trang có đang tải code nó không dùng hay không, tìm tên class của component trong các chunk mà HTML của trang đó liệt kê.

Import sau một flag false vẫn bị đóng gói

Menu toàn màn hình cũ của Perxel đã được tắt bằng một flag false, nhưng Header vẫn import nó. Bundler không bỏ import này, nên con trỏ hoạt hình và mọi trạng thái của nó vẫn có mặt trên mọi trang, trên mọi thiết bị. Tương tự, chỉ một dòng import của section cũ (không còn render) cũng đủ đưa thư viện motion vào trang chủ. Component đã tắt nên được import bằng next/dynamic, hoặc bỏ import hẳn.

Code của một loại thiết bị chỉ tải trên thiết bị đó

Thanh điều hướng dưới đáy màn hình chỉ dùng trên màn hình dưới 1024px, con trỏ hoạt hình chỉ dùng với chuột, và Lenis (cuộn mượt) chỉ cần với con lăn chuột. Mỗi phần là một chunk riêng, tải sau khi hydrate và chỉ trên thiết bị cần nó: next/dynamic với ssr: false đặt sau useMediaQuery, hoặc import() trong một effect. Riêng thanh điều hướng giúp desktop bớt 33 KB brotli.

Sau mỗi lần build, nên kiểm tra mọi đường dẫn /_next/static trong HTML đều tồn tại. Có lần Turbopack sinh ra thẻ preload cho một chunk không có thật, gây lỗi 404 trên các trang bài viết.

Mặc định, <Link> của Next.js prefetch route khi link xuất hiện trong viewport. Header và footer link tới mọi trang, nên trang chủ tải luôn JavaScript của các trang khác ngay sau khi mở. Perxel viết một component link riêng chỉ prefetch khi người dùng đưa chuột, focus hoặc chạm vào link. Lighthouse báo "unused JavaScript" giảm từ 110 KiB xuống 22 KiB. Các nút ở màn hình đầu tiên cũng dùng component này, vì một prefetch lúc tải trang có thể phải chờ server khởi động (xem phần Máy chủ).

View transition chỉ bật sau thao tác đầu tiên

Với giá trị mặc định auto, <ViewTransition> của React chạy một hiệu ứng chuyển cảnh cho mỗi lần cập nhật sau hydrate. Trên production có ba lần như vậy trong 1,5 giây đầu, mỗi lần là một ảnh chụp trang mờ dần trong 300 ms đè lên trang thật, nên header và hero trông như được vẽ hai lần. CLS vẫn là 0, nên lỗi này chỉ thấy được khi ghi lại các lần gọi startViewTransition. Bây giờ view transition chỉ bật sau thao tác đầu tiên của người dùng. Chuyển trang luôn đến sau một thao tác, nên hiệu ứng chuyển trang vẫn chạy bình thường.

Số đo trước và sau: JavaScript trang chủ từ 313 KB còn 261 KB, trang /works từ 290 KB còn 238 KB, unused JavaScript từ 110 KiB còn 22 KiB

JavaScript mỗi trang tải (brotli, ngày 9/10/2026) và mục "unused JavaScript" của Lighthouse trên trang chủ. Sau đó, trên điện thoại, trang chủ giảm thêm 27 KB và các trang khác giảm thêm 18 đến 19 KB nhờ tách code theo thiết bị.

Tài nguyên: font, ảnh và video

  • Tự host font và chỉ giữ những ký tự cần dùng. Với next/font/google, một trang tiếng Việt tải thêm cả bộ latin-ext, vì các chữ ă, đ, ơ, ư nằm trong dải ký tự của bộ này: tổng cộng 7 file font, 218 KB trước LCP. Bây giờ Inter và Manrope mỗi font chỉ còn một file, cắt bằng pyftsubset theo dải latin và tiếng Việt: Inter 53 KB, Manrope 29 KB.
  • Font nào có trong DOM cũng được tải lúc mở trang, kể cả khi nằm ở cuối trang. Font Geist chỉ dùng cho vài con số trong danh sách dịch vụ, nhưng vẫn là 29 KB tải ngay lúc đầu và làm FCP lúc 1,21 giây, lúc 1,66 giây tùy request font bắt đầu trước hay sau lần vẽ đầu tiên. Các con số chuyển sang Inter với tabular-nums và Geist được bỏ khỏi dự án.
  • Ảnh có kích thước gần với khung hiển thị. Logo giới hạn ở 128px, logo trong badge công nghệ (hiện ở 24px) có bản 48px riêng (cả bộ từ 76 KB còn 25 KB), thumbnail dự án tối đa 1100px, và thuộc tính sizes sát với chiều rộng thật của khung.
  • Poster của video là <Image> có kích thước, không dùng thuộc tính poster. Thuộc tính poster không có srcset, nên điện thoại phải tải ảnh 1100px cho một khung 365px.
  • Video nén lại theo kích thước hiển thị. Các video dự án được encode lại ở 1120px, crf 34, không có âm thanh, bật faststart: tổng dung lượng từ 20,5 MB còn 6,5 MB. Bản video ẩn chỉ dùng để giữ layout có preload="none" để không tải thêm lần nữa.
Số đo trước và sau: font tải trước LCP từ 218 KB còn 82 KB, video dự án từ 20,5 MB còn 6,5 MB, logo trong badge công nghệ từ 76 KB còn 25 KB

Dung lượng font tải trước LCP trên một trang tiếng Việt, tổng dung lượng video dự án và bộ logo trong badge công nghệ (tháng 10/2026).

Script bên thứ ba

Google Analytics và Cloudflare Web Analytics chỉ tải khi người dùng có thao tác đầu tiên (cuộn, di chuột, chạm, bấm phím, click), hoặc sau 15 giây nếu không có thao tác nào. Lúc đầu thời gian chờ là 8 giây, đủ để lần đo của PageSpeed chạy tới và tải gtag (172 KiB, trong đó 70 KiB bị báo là không dùng).

Perxel ghi rõ điều này vì nó cho thấy giới hạn của điểm lab: lần đo giả lập không cuộn hay chạm vào trang và kết thúc trước 15 giây, nên không bao giờ tính phần script này. Người xem thật vẫn tải nó ngay khi cuộn trang, sau khi trang đã hiện. Cái giá phải trả là người xem rời trang trong 15 giây đầu mà không thao tác gì sẽ không được ghi nhận. Script của Cloudflare Web Analytics được cài thủ công thay vì để Cloudflare tự chèn, vì cách tự chèn tải script ngay lúc mở trang.

Máy chủ: Cloudflare Workers không tự làm những gì Vercel làm sẵn

Trên Vercel, trang đã prerender được trả về từ edge cache mà không cần khởi động ứng dụng. Trên Cloudflare, OpenNext đóng gói cả ứng dụng Next.js thành một Worker (của Perxel là 17,5 MB), và mọi request đi qua Worker này, kể cả request lấy một trang HTML đã prerender sẵn.

Worker phải khởi động ở request đầu tiên

Với một website ít truy cập, Worker thường chưa chạy sẵn ở vị trí gần người xem. Trong một lần đo Lighthouse trên production, HTML mất 2,84 giây mới bắt đầu trả về, và mọi request khác phải chờ nó. Trên cùng một kết nối, request HTML đầu tiên chờ 0,73 giây, chín request tiếp theo chỉ 0,09 đến 0,19 giây, bằng một file CSS tĩnh. PageSpeed luôn đo như một lượt truy cập đầu tiên, và người vào từ Google cũng vậy.

Một Worker nhỏ đứng trước Next.js

Perxel đặt một Worker khoảng 40 dòng, không có code Next.js, trước Worker của OpenNext. Khi trình duyệt tải một trang đã prerender (GET hoặc HEAD, không có header RSC hay Next-Router-*, không có tham số _rsc), Worker này trả về bản HTML đã lưu sẵn trong static assets, kèm đúng các header mà OpenNext gửi. Mọi request khác (dữ liệu khi chuyển trang trong site, form, API, ảnh, OG image, trang 404, redirect) được chuyển tiếp sang Worker của OpenNext qua service binding. Cách làm này giống cách Vercel và OpenNext trên AWS xử lý trang tĩnh, nhưng OpenNext trên Cloudflare chưa có sẵn nên Perxel tự viết.

Sơ đồ đường đi của một request tới perxel.com: file tĩnh được trả từ static assets, trang đã prerender được Worker nhỏ trả bản HTML lưu sẵn, mọi request còn lại đi tới Worker của OpenNext

Đường đi của một request: tầng static assets, Worker nhỏ đứng trước, rồi mới tới ứng dụng Next.js.

Thời gian chờ server: request đầu tiên tới Worker của OpenNext mất 4.007 ms, tới Worker nhỏ mất 418 ms; trung vị của 8 request là 532 ms và 115 ms

Thời gian chờ server trên production, đo bằng curl ngày 10/10/2026, mỗi loại 8 request trong cùng một phút. Hai loại request không hoàn toàn giống nhau: trang HTML đi qua Worker nhỏ, dữ liệu chuyển trang (RSC: 1) đi tới OpenNext.

Worker nhỏ này dựa vào những chi tiết nội bộ của Next.js và OpenNext, không phải API công khai: các header mà router dùng khi lấy dữ liệu chuyển trang, cấu trúc thư mục build, và các header OpenNext gửi kèm trang. Nếu một bản nâng cấp đổi những chi tiết này, link trong site có thể hỏng mà build vẫn chạy bình thường. Vì vậy sau mỗi lần nâng cấp Next.js hoặc OpenNext, Perxel chạy cả hai Worker trên máy và kiểm tra theo danh sách: trang có được trả từ Worker nhỏ không, request RSC có đi tới OpenNext không, HTML có giống hệt bản của OpenNext không, trang 404 và redirect có đúng không. Sau mỗi lần deploy, một script tự kiểm tra lại các điểm này trên production. Nếu có lỗi, chỉ cần trỏ domain về lại Worker của OpenNext trong vài giây, vì Worker đó vẫn chạy được toàn bộ site một mình.

Những thiết lập khác trên Workers

  • Cache dài hạn cho /_next/static. File trong thư mục này có hash trong tên, nên có thể đặt max-age=31536000, immutable trong _headers. Mặc định là max-age=0, must-revalidate, nên mỗi lần quay lại trang, trình duyệt phải hỏi lại server cho từng chunk.
  • Bật binding IMAGES cho /_next/image. Nếu thiếu, OpenNext trả ảnh gốc không resize: Lighthouse từng báo một ảnh 1440px cho khung 634px.
  • Không có request tới OpenNext lúc tải trang. Mọi request không được prerender (/api/*, dữ liệu chuyển trang) đều làm Worker của OpenNext khởi động, có lúc mất 1 đến 3 giây. Badge trạng thái ở footer từng gọi API hai lần lúc mở trang. Bây giờ nó gọi một lần, 1,5 giây sau khi trang tải xong hoặc khi người dùng có thao tác đầu tiên.
  • Không đọc file lúc chạy. Workers không có ổ đĩa, và OpenNext có thể chạy lại module của trang tĩnh khi cache trống. Một lệnh fs.readdirSync chạy được lúc next build vẫn sẽ lỗi trên production. Danh sách bài viết được liệt kê sẵn trong code.

Những cách đã thử rồi bỏ

  • experimental.inlineCss. FCP đo bằng throttling thật giảm từ 2,1 xuống 0,8 giây, nhưng điểm PageSpeed giảm từ 88 xuống 83 và LCP giả lập tăng khoảng 1 giây. CSS bị chèn hai lần (trong HTML và trong RSC payload), và chế độ giả lập tính phần HTML lớn hơn vào LCP.
  • Cache Rule của Cloudflare cho HTML. Khi route có Worker, Worker chạy trước và Cache Rule không có tác dụng. Nếu có tác dụng thì cũng rủi ro, vì HTML cũ trong cache sẽ trỏ tới các chunk đã bị xóa ở lần deploy sau.
  • Hoãn GSAP tới sau phần hero. GSAP chỉ chiếm 44 KB trong 601 KB tải trước LCP, trong khi phải sửa khoảng 30 module. Perxel đo trước và quyết định không làm.
  • Trả HTML từ static assets ngay trong Worker lớn. Cách này chỉ nhanh hơn khoảng 55 ms ở trung vị, vì Worker lớn vẫn phải khởi động. Lần thử này lấy trung vị của các request đã "nóng", nên không thấy được vấn đề khởi động. Worker nhỏ ở trên giải quyết đúng phần đó.

Checklist cho dự án Next.js tiếp theo

  • Đo bằng Lighthouse cùng phiên bản PageSpeed, lấy trung vị, so sánh từng cặp, và kiểm tra LCP bằng throttling thật.
  • Phần tử LCP hiện bằng HTML và CSS, không chờ JavaScript.
  • Phần layout do JavaScript thay đổi có trạng thái ban đầu trong CSS.
  • Trên điện thoại, thay hiệu ứng nặng bằng phiên bản nhẹ thay vì chỉ ẩn bằng CSS.
  • Section nặng ở cuối trang chỉ tải dữ liệu khi người dùng tới gần hoặc có thao tác đầu tiên.
  • Kiểm tra client component trong cây module của mọi server component dùng chung (layout, header, footer).
  • Code của một loại thiết bị nằm trong chunk riêng, sau useMediaQuery hoặc import().
  • Link luôn hiện trên màn hình chỉ prefetch khi người dùng có ý định bấm.
  • Font tự host, cắt theo dải ký tự, và không thêm font cho vài nhãn nhỏ.
  • Script bên thứ ba tải sau thao tác đầu tiên, và ghi rõ đánh đổi.
  • Trên Cloudflare Workers: trang tĩnh không đi qua Worker lớn, /_next/static cache dài hạn, bật binding IMAGES, không dùng fs.

Câu hỏi thường gặp

Chạy trên Vercel thì có cần Worker đứng trước không?

Không cần. Vercel trả trang đã prerender từ edge cache mà không khởi động ứng dụng, nên vấn đề khởi động ở request đầu tiên gần như không xảy ra với trang tĩnh. Các phần về giao diện, code, tài nguyên và script bên thứ ba vẫn áp dụng như nhau.

Có nên chạy theo điểm PageSpeed không?

Perxel không hứa điểm số cho website nào. Như hình ở đầu bài, cùng một phiên bản code có thể được 68 hoặc 95 điểm. Điểm còn phụ thuộc vào nội dung đăng lên sau này, hosting và các script bên thứ ba. Mục tiêu thực tế hơn là để phần tử LCP hiện sớm, trang không xê dịch, và mỗi trang không tải code nó không dùng. Khi website có đủ lượt truy cập, dữ liệu CrUX là con số nên xem trước. Bài PageSpeed Insights & Core Web Vitals có ba trường hợp thật cho thấy điểm lab và dữ liệu người dùng thật có thể chênh nhau thế nào.

Bản điện thoại có bị mất hiệu ứng không?

Nội dung giữ nguyên trên mọi thiết bị. Những hiệu ứng cần con trỏ (mép nước gợn theo chuột, con trỏ hoạt hình) chỉ có trên desktop. Những hiệu ứng nặng (canvas mép nước, hàng card uốn cong) có phiên bản nhẹ hơn hoặc chuyển thành layout thường trên điện thoại.

Footnotes

Bạn muốn nhận báo giá?
Hãy liên hệ Perxel ngay hôm nay.