Optimize GradientBlinds: Pause animation when off-screen #3

Merged
google-labs-jules[bot] merged 1 commits from perf/optimize-gradient-blinds-536844470945230510 into main 2026-01-23 09:05:16 +00:00
2 changed files with 132 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
// @vitest-environment jsdom
import { render, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import GradientBlinds from './GradientBlinds';
// Mock ogl
vi.mock('ogl', () => {
return {
Renderer: class {
gl = {
// Use a real canvas element so appendChild works
canvas: document.createElement('canvas'),
drawingBufferWidth: 100,
drawingBufferHeight: 100,
};
setSize() {}
render() {}
},
Program: class {
remove() {}
},
Mesh: class {
remove() {}
},
Triangle: class {
remove() {}
},
};
});
// Mock ResizeObserver
globalThis.ResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
};
describe('GradientBlinds', () => {
let rafSpy: any;
let cancelRafSpy: any;
let ioCallback: (entries: IntersectionObserverEntry[]) => void;
let observeSpy: any;
let disconnectSpy: any;
beforeEach(() => {
rafSpy = vi.spyOn(window, 'requestAnimationFrame').mockImplementation((_cb: any) => {
return 123;
});
cancelRafSpy = vi.spyOn(window, 'cancelAnimationFrame').mockImplementation((_id: number) => {
});
// Mock IntersectionObserver
observeSpy = vi.fn();
disconnectSpy = vi.fn();
(globalThis as any).IntersectionObserver = class IntersectionObserver {
constructor(cb: any) {
ioCallback = cb;
}
observe = observeSpy;
unobserve = vi.fn();
disconnect = disconnectSpy;
};
});
afterEach(() => {
vi.clearAllMocks();
});
it('starts animation loop on mount', () => {
const { unmount } = render(<GradientBlinds />);
expect(rafSpy).toHaveBeenCalled();
unmount();
expect(cancelRafSpy).toHaveBeenCalled();
});
it('pauses animation loop when off-screen and resumes when on-screen', () => {
const { unmount } = render(<GradientBlinds />);
// Initial start
expect(rafSpy).toHaveBeenCalledTimes(1);
// Reset spies to check for subsequent calls
rafSpy.mockClear();
cancelRafSpy.mockClear();
// Simulate off-screen
act(() => {
if (ioCallback) {
ioCallback([{ isIntersecting: false } as IntersectionObserverEntry]);
}
});
// Should cancel animation frame
expect(cancelRafSpy).toHaveBeenCalled();
// Should NOT request new frame (loop stopped)
expect(rafSpy).not.toHaveBeenCalled();
// Reset spies
cancelRafSpy.mockClear();
// Simulate on-screen
act(() => {
if (ioCallback) {
ioCallback([{ isIntersecting: true } as IntersectionObserverEntry]);
}
});
// Should restart animation loop
expect(rafSpy).toHaveBeenCalled();
unmount();
expect(disconnectSpy).toHaveBeenCalled();
});
});

View File

@@ -333,11 +333,28 @@ void main() {
}
}
};
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
if (!rafRef.current) {
lastTimeRef.current = 0;
rafRef.current = requestAnimationFrame(loop);
}
} else {
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
}
});
observer.observe(container);
rafRef.current = requestAnimationFrame(loop);
return () => {
if (rafRef.current) cancelAnimationFrame(rafRef.current);
canvas.removeEventListener('pointermove', onPointerMove);
observer.disconnect();
ro.disconnect();
if (canvas.parentElement === container) {
container.removeChild(canvas);