fix(gallery): give the Grid layout a lazy-loading pre-load band (#1287)
Grid was the only layout passing `lazy` without an `inViewRootMargin`, so PhotoCard ran its observer at the IntersectionObserver default of `0px` with `threshold: 0.1`. A tile could not begin loading until a tenth of it was already on screen — there was no lead at all. The gallery owner's account of the symptom is that defect's exact shape: spinning the scroll wheel outran loading by roughly 50 images, then it caught up. Outrun-then-recover is what a zero-width pre-load band looks like from a chair. This is the one thing in that investigation that does not rest on the reporter's instrumented runs, which they have since withdrawn after finding their automation harness ran in a hidden pane — `innerHeight: 0`, so nothing could intersect and no tile could ever load. The missing margin is visible in the source regardless. Percent, not vh. `rootMargin` accepts only px and percentages, and a `vh` value throws SyntaxError at construction, which would have taken down every Grid gallery. Verified in Chrome: '100% 0px' → accepted '100px 0px' → accepted '100vh 0px' → SyntaxError: rootMargin must be specified in pixels or percent A percentage resolves against the root's own box, so 100% is one viewport height of lead in each direction — viewport-relative, which a fixed 100px like Justified's is not. A phone and a 4K desktop scroll past very different amounts of grid per gesture. Deliberately NOT included: a sweep for cards left un-loaded after scrolling settles. That was aimed at permanent loss from `triggerOnce`, and the owner's observation that tiles do come back on desktop argues against it. Complexity chasing a symptom nobody has reproduced outside a broken harness. Three guard tests, including one on the unit, since the failure mode of getting that wrong is a gallery that does not render at all.
This commit is contained in:
@@ -84,6 +84,24 @@ const GridPhoto: React.FC<GridPhotoProps> = ({
|
||||
onToggleSelect={onToggleSelect}
|
||||
className={`photo-card relative group cursor-pointer aspect-square ${animationClass}`}
|
||||
lazy
|
||||
/*
|
||||
* Pre-load band (#1287). Grid was the only lazy layout passing no
|
||||
* `inViewRootMargin`, so PhotoCard ran the observer at the
|
||||
* IntersectionObserver default of 0px with threshold 0.1 — a tile could
|
||||
* not begin loading until a tenth of it was already on screen. The
|
||||
* gallery owner's description of the symptom is that exact shape:
|
||||
* spinning the wheel outran loading by ~50 images, then it caught up.
|
||||
*
|
||||
* Viewport-relative rather than a fixed 100px like Justified: a phone
|
||||
* and a 4K desktop scroll past very different amounts of grid per
|
||||
* gesture, and a band tuned to one is wrong for the other.
|
||||
*
|
||||
* `%`, not `vh` — rootMargin only accepts px and percentages, and an
|
||||
* IntersectionObserver constructed with a vh value throws. A percentage
|
||||
* resolves against the root's own box, so 100% is one viewport height
|
||||
* of lead in each direction, which is what vh would have meant.
|
||||
*/
|
||||
inViewRootMargin="100% 0px"
|
||||
fadeInWhenVisible={animationType === 'fade'}
|
||||
skeletonClassName="skeleton aspect-square w-full rounded-lg"
|
||||
imageProps={{
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Grid's lazy-loading pre-load band (#1287).
|
||||
*
|
||||
* Grid was the only layout passing `lazy` without an `inViewRootMargin`, so
|
||||
* PhotoCard ran its observer at the IntersectionObserver default of `0px`
|
||||
* with `threshold: 0.1` — a tile could not begin loading until a tenth of it
|
||||
* was already on screen. The gallery owner described exactly that: spinning
|
||||
* the scroll wheel outran loading by ~50 images before it caught up.
|
||||
*
|
||||
* The unit matters as much as the value. `rootMargin` accepts only px and
|
||||
* percentages; an IntersectionObserver constructed with a `vh` value throws
|
||||
* SyntaxError, which would have broken every Grid gallery outright. Verified
|
||||
* in Chrome:
|
||||
*
|
||||
* '100% 0px' → accepted
|
||||
* '100px 0px' → accepted
|
||||
* '100vh 0px' → SyntaxError: rootMargin must be specified in pixels or percent
|
||||
*
|
||||
* jsdom has no IntersectionObserver, so this asserts against the source
|
||||
* rather than constructing one.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
|
||||
const layouts = resolve(__dirname, '..');
|
||||
const read = (f: string) => readFileSync(resolve(layouts, f), 'utf8');
|
||||
|
||||
/** Only px and % are legal rootMargin units. */
|
||||
const LEGAL_ROOT_MARGIN = /^(-?\d+(px|%)|0)(\s+(-?\d+(px|%)|0)){0,3}$/;
|
||||
|
||||
describe('grid lazy pre-load band', () => {
|
||||
it('Grid passes an inViewRootMargin', () => {
|
||||
expect(read('GridGalleryLayout.tsx')).toMatch(/inViewRootMargin=/);
|
||||
});
|
||||
|
||||
it('every inViewRootMargin in every layout uses a legal unit', () => {
|
||||
// A vh value throws at IntersectionObserver construction and takes the
|
||||
// whole gallery down with it, so this guards the unit, not just presence.
|
||||
for (const file of ['GridGalleryLayout.tsx', 'JustifiedGalleryLayout.tsx']) {
|
||||
const src = read(file);
|
||||
for (const [, value] of src.matchAll(/inViewRootMargin="([^"]+)"/g)) {
|
||||
expect(value, `${file}: "${value}"`).toMatch(LEGAL_ROOT_MARGIN);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('every layout that lazy-renders also declares a pre-load band', () => {
|
||||
// The defect was Grid being lazy with no margin. Any future layout that
|
||||
// opts into `lazy` and forgets the margin reintroduces it.
|
||||
for (const file of ['GridGalleryLayout.tsx', 'JustifiedGalleryLayout.tsx']) {
|
||||
const src = read(file);
|
||||
const isLazy = /^\s*lazy\s*$/m.test(src) || /\slazy=\{?true/.test(src);
|
||||
if (!isLazy) continue;
|
||||
expect(src, `${file} is lazy but declares no inViewRootMargin`)
|
||||
.toMatch(/inViewRootMargin=/);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user