From cea9efab993cfed8fafaa5a2467c0ad2419a1c74 Mon Sep 17 00:00:00 2001 From: tuyenio Date: Thu, 25 Jun 2026 08:44:24 +0700 Subject: [PATCH] =?UTF-8?q?perf:=20ti=E1=BB=87n=20=C3=ADch=20render-gate?= =?UTF-8?q?=20(d=E1=BB=ABng=20v=C3=B2ng=20l=E1=BA=B7p=20v=E1=BA=BD=20khi?= =?UTF-8?q?=20ngo=C3=A0i=20m=C3=A0n=20h=C3=ACnh/=C4=91=E1=BB=95i=20tab)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/lib/render-gate.ts | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/app/lib/render-gate.ts diff --git a/src/app/lib/render-gate.ts b/src/app/lib/render-gate.ts new file mode 100644 index 0000000..b145586 --- /dev/null +++ b/src/app/lib/render-gate.ts @@ -0,0 +1,39 @@ +// src/app/lib/render-gate.ts +// Chỉ cho phép vòng lặp vẽ (rAF/setAnimationLoop) chạy khi phần tử nằm trong vùng nhìn +// VÀ tab đang hiển thị. Gọi onChange(active) mỗi khi trạng thái đổi. Trả về hàm dọn dẹp. +export function createRenderGate( + element: Element, + onChange: (active: boolean) => void, +): () => void { + let onScreen = false; + let visible = typeof document === "undefined" ? true : !document.hidden; + let last = false; + + const emit = () => { + const next = onScreen && visible; + if (next !== last) { + last = next; + onChange(next); + } + }; + + const io = new IntersectionObserver( + (entries) => { + onScreen = entries.some((entry) => entry.isIntersecting); + emit(); + }, + { threshold: 0.01 }, + ); + io.observe(element); + + const onVisibility = () => { + visible = !document.hidden; + emit(); + }; + document.addEventListener("visibilitychange", onVisibility); + + return () => { + io.disconnect(); + document.removeEventListener("visibilitychange", onVisibility); + }; +}