diff --git a/apps/web/app/entry.client.tsx b/apps/web/app/entry.client.tsx index 9c665ede072..ddb7bede901 100644 --- a/apps/web/app/entry.client.tsx +++ b/apps/web/app/entry.client.tsx @@ -4,6 +4,7 @@ * See the LICENSE file for details. */ +import "@/lib/polyfills"; import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; import { HydratedRouter } from "react-router/dom"; diff --git a/apps/web/core/components/core/render-if-visible-HOC.tsx b/apps/web/core/components/core/render-if-visible-HOC.tsx index bd8a77a04a2..889b2a928fd 100644 --- a/apps/web/core/components/core/render-if-visible-HOC.tsx +++ b/apps/web/core/components/core/render-if-visible-HOC.tsx @@ -7,6 +7,7 @@ import type { ReactNode, MutableRefObject } from "react"; import React, { useState, useRef, useEffect } from "react"; import { cn } from "@plane/utils"; +import { scheduleIdleCallback } from "@/lib/polyfills"; type Props = { defaultHeight?: string; @@ -23,6 +24,10 @@ type Props = { forceRender?: boolean; }; +/** + * Renders children only when the element intersects the viewport (or is forced visible), using a placeholder and + * optional height recording to reduce work for long lists. + */ function RenderIfVisible(props: Props) { const { defaultHeight = "300px", @@ -51,8 +56,8 @@ function RenderIfVisible(props: Props) { const observer = new IntersectionObserver( (entries) => { //DO no remove comments for future - if (typeof window !== undefined && window.requestIdleCallback && useIdletime) { - window.requestIdleCallback(() => setShouldVisible(entries[entries.length - 1].isIntersecting), { + if (typeof window !== "undefined" && useIdletime) { + scheduleIdleCallback(() => setShouldVisible(entries[entries.length - 1].isIntersecting), { timeout: 300, }); } else { @@ -66,18 +71,15 @@ function RenderIfVisible(props: Props) { ); observer.observe(intersectionRef.current); return () => { - if (intersectionRef.current) { - // eslint-disable-next-line react-hooks/exhaustive-deps - observer.unobserve(intersectionRef.current); - } + observer.disconnect(); }; } - }, [intersectionRef, children, root, verticalOffset, horizontalOffset]); + }, [intersectionRef, root, verticalOffset, horizontalOffset, useIdletime]); //Set height after render useEffect(() => { if (intersectionRef.current && isVisible && shouldRecordHeights) { - window.requestIdleCallback(() => { + scheduleIdleCallback(() => { if (intersectionRef.current) placeholderHeight.current = `${intersectionRef.current.offsetHeight}px`; }); } diff --git a/apps/web/core/lib/polyfills/index.ts b/apps/web/core/lib/polyfills/index.ts index 2243dc620c2..641206f7ea4 100644 --- a/apps/web/core/lib/polyfills/index.ts +++ b/apps/web/core/lib/polyfills/index.ts @@ -4,8 +4,16 @@ * See the LICENSE file for details. */ -if (typeof window !== "undefined" && window) { - // Add request callback polyfill to browser in case it does not exist +/** + * Ensures `window.requestIdleCallback` and `window.cancelIdleCallback` exist. + * Installs minimal shims when the browser omits them (e.g. older Safari / WebKit). + * Safe to call repeatedly; only assigns missing APIs once. + */ +function ensureRequestIdleCallbackPolyfilled(): void { + if (typeof window === "undefined" || !window) { + return; + } + window.requestIdleCallback = window.requestIdleCallback ?? function (cb) { @@ -27,4 +35,19 @@ if (typeof window !== "undefined" && window) { }; } -export {}; +ensureRequestIdleCallbackPolyfilled(); + +/** + * Schedules work to run when the browser is idle, or after a short delay when idle scheduling is unavailable. + * + * @param callback - Invoked with an `IdleDeadline`-like object (native or polyfilled). + * @param options - Optional `timeout` forwarded to the native API when present. + * @returns An idle handle for cancellation, or `0` when `window` is undefined (SSR). + */ +export function scheduleIdleCallback(callback: IdleRequestCallback, options?: IdleRequestOptions): number { + ensureRequestIdleCallbackPolyfilled(); + if (typeof window === "undefined") { + return 0; + } + return window.requestIdleCallback(callback, options); +}