CSS 3D from Scratch · Part 5 — Lighting, Performance & Accessibility
The production finish for CSS 3D: fake directional lighting so objects look solid, keep animations smooth on the GPU compositor, fix z-fighting, honor prefers-reduced-motion, add fallbacks, and debug 3D when it breaks. With a live demo.
Bài học cuối, đồ đệ. Con đã dựng được cube, card nghiêng, và carousel. Nhưng có một hẻm núi rộng giữa “chạy trong demo của tôi” và “ship cho người dùng thật trên thiết bị thật.” Hôm nay sư phụ dạy con băng qua nó: ánh sáng, tốc độ, và sự tử tế.
Đây là thứ phân biệt một CodePen lòe loẹt với một hiệu ứng 3D con có thể đưa vào production mà không phải xin lỗi.
Bật/tắt shading, will-change, z-fighting, và reduced-motion trong demo khi con đọc.
Mở demo đầy đủ:
1. Ánh sáng — vì CSS không có
Một sự thật khiêm tốn: CSS 3D không có đèn. Một cube xoay mà mọi mặt cùng một màu phẳng trông như cục bùng nhùng — mắt con không phân biệt được các mặt. Bật shading: off trong demo và xem nó tan thành vô nghĩa.
Cách sửa là giả một nguồn sáng. Chọn một hướng sáng tưởng tượng (ví dụ top-front), rồi làm tối mỗi mặt tỉ lệ với mức nó quay lưng lại nguồn sáng đó. Kỹ thuật đơn giản nhất: một lớp phủ đen bán trong suốt cho mỗi mặt.
.face::after {
content: "";
position: absolute; inset: 0;
background: #000; /* shadow overlay */
}
/* light from top-front: top brightest, bottom & back darkest */
.face.top::after { opacity: 0; }
.face.front::after { opacity: 0.05; }
.face.right::after { opacity: 0.30; }
.face.left::after { opacity: 0.42; }
.face.back::after { opacity: 0.55; }
.face.bottom::after { opacity: 0.62; }
Cái dải độ sáng khác nhau giữa các mặt đó chính là toàn bộ ảo giác về độ đặc. Cho vẻ cong hoặc cao cấp, thêm:
- một
linear-gradienttrên mỗi mặt thay cho màu phẳng (cho vẻ bóng), - các filter
brightness()/contrast()để chỉnh mặt, - một
box-shadowmềm hoặc “bóng tiếp xúc” radial mờ dưới vật thể để nó đặt trên bề mặt thay vì lơ lửng.
2. Hiệu năng — ở lại trên compositor
Sự thật hiệu năng quan trọng nhất trong toàn bộ CSS animation:
transformvàopacityđược GPU compositor animate — không layout, không repaint. Gần như mọi thứ khác (top, left, width, margin…) buộc trình duyệt tính lại trang mỗi khung hình.
May cho con: toàn bộ CSS 3D chính là transform. Nên 3D vốn nhanh — nếu con không tự phá. Quy tắc:
- Chỉ animate
transform/opacity. Đừng bao giờ animateleft/top/widthđể dời vật 3D. will-change: transformnâng phần tử lên lớp GPU riêng trước khi nó di chuyển, diệt cú giật khung hình đầu:
.spinning-thing { will-change: transform; }
Nhưng — sư phụ nghiêm khắc chỗ này — will-change không phải nút “nhanh lên” thần kỳ. Mỗi lớp được nâng ngốn bộ nhớ GPU. Đặt nó lên 200 phần tử và con sẽ phá sập hiệu năng, không phải tăng. Chỉ dùng trên vài thứ đang animate, và bỏ khi chúng dừng.
Mở DevTools › Layers để thấy phần tử nào có lớp riêng.
3. Z-fighting — khi các mặt cãi nhau
Bật panel z-fighting trong demo: hai panel phẳng ở đúng cùng độ sâu nhấp nháy và rách khi góc nhìn đổi. GPU thật sự không quyết được cái nào ở trên, nên chọn khác nhau từng pixel, từng khung hình.
Cách sửa rất đơn giản khi đã biết — tách chúng ra một sợi tóc:
.panel-on-top { transform: translateZ(1px); } /* 1px is enough to settle the fight */
Con sẽ gặp nó mỗi khi xếp một nhãn lên một mặt, hoặc chồng hai mặt đồng phẳng. Nhớ triệu chứng (lung linh/rách) và thuốc chữa (translateZ(1px)).
4. Khả năng tiếp cận — chuyển động có thể gây hại
Cái này không tùy chọn, đồ đệ. Chuyển động 3D lớn — xoay, parallax, zoom — có thể gây buồn nôn, chóng mặt, đau nửa đầu thật cho người có rối loạn tiền đình. Nền tảng cho con cách tôn trọng họ:
@media (prefers-reduced-motion: reduce) {
.spinning-thing { animation: none; }
.tilt-card { transition: none; }
}
Đừng chỉ xóa chuyển động rồi để lại phần tử chết — khi hợp lý, thay nó bằng một lựa chọn nhẹ nhàng (trạng thái tĩnh, hoặc cross-fade nhanh thay cho flip). Và với hiệu ứng do JS (cái nghiêng ở Phần 3), kiểm tra cùng thiết lập đó trước khi gắn handler:
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!reduce) stage.addEventListener('pointermove', tilt);
Ngoài ra: 3D là trang trí. Giữ nội dung thật theo thứ tự DOM bình thường, gắn nhãn aria-* cho 3D tương tác, và đảm bảo người dùng bàn phím điều khiển được carousel (Phần 4).
5. Fallback & gỡ lỗi
Hỗ trợ CSS 3D hôm nay rất tốt, nhưng hãy chuyên nghiệp:
- Dùng feature query cho phần xịn nếu con phụ thuộc cái mới, và ship bản phẳng nếu không:
@supports (transform-style: preserve-3d) {
.card { transform-style: preserve-3d; }
}
- Suy giảm duyên dáng: card phẳng vẫn phải đọc và dùng được khi không có 3D — 3D là phần nâng cao, không phải nội dung.
Khi 3D hỏng (và nó sẽ hỏng), chạy checklist của sư phụ:
perspectiveở cha chưa? Không máy ảnh = phẳng. (Phần 1)transform-style: preserve-3dđặt ở mọi container 3D chưa? Không di truyền. (Phần 2)- Có gì làm dẹp phẳng nó?
overflow,filter,opacity < 1,clip-path, hoặcmasktrên cha 3D ép vềflat. - Mặt sau lộ ra? Thêm
backface-visibility: hidden. (Phần 1–2) - Nhấp nháy? Z-fighting —
translateZ(1px).
Thuộc danh sách đó. Nó giải quyết ~95% khoảnh khắc “CSS 3D của tôi hỏng rồi”.
Luyện tập, không thì coi như chưa học
- Chiếu sáng cube: lấy cube Phần 2 và thêm lớp phủ shading mỗi mặt cho nguồn sáng top-front. Rồi dời sáng xuống bottom-left bằng cách hoán đổi opacity.
- Thêm bóng tiếp xúc: đặt một elip tối mờ dưới vật thể 3D để nó trông có điểm tựa.
- Làm mọi thứ an toàn reduced-motion: xem lại card nghiêng và carousel; thêm media query và guard JS để chúng dịu lại khi OS yêu cầu.
- Săn lỗi dẹp phẳng: cố tình thêm
overflow: hiddenvào một cảnh 3D đang chạy, xác nhận nó dẹp, rồi sửa. Khắc cốt checklist.
Sư phụ cúi chào
Năm phần trước con còn không biết trục Z tồn tại. Giờ con đặt được máy ảnh perspective, dựng vật thể đặc với preserve-3d, làm chúng nghiêng theo con trỏ với parallax phân lớp, xoay thành carousel và coverflow, và hoàn thiện bằng ánh sáng, hiệu năng, khả năng tiếp cận, và fallback duyên dáng.
Đó là toàn bộ nghề CSS 3D — không WebGL, không thư viện, chỉ perspective, transform-style, translateZ, và bộ ba xoay, dùng có gu.
Giờ hãy đi dựng thứ mà web chưa từng thấy. Sư phụ đã dạy hết những gì có thể; phần còn lại là luyện tập.