perf(glass): skip hidden scroll surface output during native scrolling

This commit is contained in:
InfinityPacer
2026-09-06 06:45:26 +08:00
parent 80acc23f67
commit 5704ce6017
2 changed files with 214 additions and 77 deletions
@@ -3037,6 +3037,14 @@ describe('glass optical surface discovery', () => {
callbacks.clear() callbacks.clear()
scheduledCallbacks.forEach(callback => callback(performance.now())) scheduledCallbacks.forEach(callback => callback(performance.now()))
} }
const mainScene = render.mock.calls
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
.find(scene => scene.children[0]?.material?.uniforms.uPointer)
if (!mainScene) throw new Error('main optical scene was not rendered')
const uniforms = mainScene.children[0].material!.uniforms as unknown as {
uPointer: { value: { x: number; y: number } }
uTrail: { value: Array<{ x: number; y: number; z: number }> }
}
render.mockClear() render.mockClear()
dispatchTouchEvent('touchstart', [{ clientX: 80, clientY: 180, identifier: 7 }]) dispatchTouchEvent('touchstart', [{ clientX: 80, clientY: 180, identifier: 7 }])
@@ -3050,18 +3058,9 @@ describe('glass optical surface discovery', () => {
callbacks.clear() callbacks.clear()
interactionFrames.forEach(callback => callback(moveEvent.timeStamp + 16)) interactionFrames.forEach(callback => callback(moveEvent.timeStamp + 16))
const scene = render.mock.calls.at(-1)?.[0] as unknown as { expect(render).not.toHaveBeenCalled()
children: Array<{ const pointer = uniforms.uPointer.value
material: { const trail = uniforms.uTrail.value
uniforms: {
uPointer: { value: { x: number; y: number } }
uTrail: { value: Array<{ x: number; y: number; z: number }> }
}
}
}>
}
const pointer = scene.children[0].material.uniforms.uPointer.value
const trail = scene.children[0].material.uniforms.uTrail.value
expect(pointer.x).toBeCloseTo(180 / 390) expect(pointer.x).toBeCloseTo(180 / 390)
expect(pointer.y).toBeCloseTo(1 - 320 / 844) expect(pointer.y).toBeCloseTo(1 - 320 / 844)
expect(trail[0]).toMatchObject({ z: 1 }) expect(trail[0]).toMatchObject({ z: 1 })
@@ -3633,6 +3632,16 @@ describe('glass optical surface discovery', () => {
scheduledCallbacks.forEach(callback => callback(performance.now())) scheduledCallbacks.forEach(callback => callback(performance.now()))
} }
expect(callbacks.size).toBe(0) expect(callbacks.size).toBe(0)
const mainScene = render.mock.calls
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
.find(scene => scene.children[0]?.material?.uniforms.uScrollOffset)
if (!mainScene) throw new Error('main optical scene was not rendered')
const settledUniforms = mainScene.children[0].material!.uniforms as unknown as {
uMotion: { value: number }
uRects: { value: Array<{ y: number }> }
uScrollOffset: { value: { y: number } }
uTrail: { value: Array<{ z: number }> }
}
vi.spyOn(window, 'scrollY', 'get').mockReturnValue(120) vi.spyOn(window, 'scrollY', 'get').mockReturnValue(120)
setOpticalSurfaceBounds(surface, { height: 500, width: 350, x: 20, y: -20 }) setOpticalSurfaceBounds(surface, { height: 500, width: 350, x: 20, y: -20 })
@@ -3643,19 +3652,6 @@ describe('glass optical surface discovery', () => {
scheduledCallbacks.forEach(callback => callback(performance.now() + 160 + pass * 16)) scheduledCallbacks.forEach(callback => callback(performance.now() + 160 + pass * 16))
} }
const settledScene = render.mock.calls.at(-1)?.[0] as unknown as {
children: Array<{
material: {
uniforms: {
uMotion: { value: number }
uRects: { value: Array<{ y: number }> }
uScrollOffset: { value: { y: number } }
uTrail: { value: Array<{ z: number }> }
}
}
}>
}
const settledUniforms = settledScene.children[0].material.uniforms
expect(settledUniforms.uMotion.value).toBe(0) expect(settledUniforms.uMotion.value).toBe(0)
expect(settledUniforms.uTrail.value.every(trail => trail.z === 0)).toBe(true) expect(settledUniforms.uTrail.value.every(trail => trail.z === 0)).toBe(true)
expect(settledUniforms.uRects.value[0].y).toBeCloseTo(1 - (100 + 500) / 844) expect(settledUniforms.uRects.value[0].y).toBeCloseTo(1 - (100 + 500) / 844)
@@ -3705,6 +3701,15 @@ describe('glass optical surface discovery', () => {
scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16)) scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16))
} }
expect(callbacks.size).toBe(0) expect(callbacks.size).toBe(0)
const mainScene = render.mock.calls
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
.find(scene => scene.children[0]?.material?.uniforms.uScrollOffset)
if (!mainScene) throw new Error('main optical scene was not rendered')
const uniforms = mainScene.children[0].material!.uniforms as unknown as {
uMotion: { value: number }
uScrollOffset: { value: { y: number } }
uTrail: { value: Array<{ z: number }> }
}
render.mockClear() render.mockClear()
scrollY = 80 scrollY = 80
@@ -3719,18 +3724,6 @@ describe('glass optical surface discovery', () => {
callbacks.clear() callbacks.clear()
scheduledCallbacks.forEach(callback => { scheduledCallbacks.forEach(callback => {
callback(performance.now() + 100 + pass * 16) callback(performance.now() + 100 + pass * 16)
const scene = render.mock.calls.at(-1)?.[0] as unknown as {
children: Array<{
material: {
uniforms: {
uMotion: { value: number }
uScrollOffset: { value: { y: number } }
uTrail: { value: Array<{ z: number }> }
}
}
}>
}
const uniforms = scene.children[0].material.uniforms
renderedScrollOffsets.push(uniforms.uScrollOffset.value.y) renderedScrollOffsets.push(uniforms.uScrollOffset.value.y)
expect(uniforms.uMotion.value).toBe(0) expect(uniforms.uMotion.value).toBe(0)
expect(uniforms.uTrail.value.every(trail => trail.z === 0)).toBe(true) expect(uniforms.uTrail.value.every(trail => trail.z === 0)).toBe(true)
@@ -3763,21 +3756,6 @@ describe('glass optical surface discovery', () => {
vi.spyOn(window, 'scrollY', 'get').mockImplementation(() => scrollY) vi.spyOn(window, 'scrollY', 'get').mockImplementation(() => scrollY)
const three = await import('three') const three = await import('three')
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render') const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
const wallpaperSampling: number[] = []
render.mockImplementation(scene => {
const uniforms = (
scene as unknown as {
children: Array<{
material?: {
uniforms?: {
uHasWallpaperTexture?: { value: number }
}
}
}>
}
).children[0]?.material?.uniforms
if (uniforms?.uHasWallpaperTexture) wallpaperSampling.push(uniforms.uHasWallpaperTexture.value)
})
const callbacks = new Map<number, FrameRequestCallback>() const callbacks = new Map<number, FrameRequestCallback>()
let frameId = 0 let frameId = 0
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(callback => { vi.spyOn(window, 'requestAnimationFrame').mockImplementation(callback => {
@@ -3808,11 +3786,16 @@ describe('glass optical surface discovery', () => {
callbacks.clear() callbacks.clear()
scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16)) scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16))
} }
expect(wallpaperSampling.at(-1)).toBe(1) const mainScene = render.mock.calls
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
.find(scene => scene.children[0]?.material?.uniforms.uHasWallpaperTexture)
if (!mainScene) throw new Error('main optical scene was not rendered')
const wallpaperSampling = mainScene.children[0].material!.uniforms.uHasWallpaperTexture
expect(wallpaperSampling.value).toBe(1)
window.dispatchEvent(new WheelEvent('wheel', { deltaY: 80 })) window.dispatchEvent(new WheelEvent('wheel', { deltaY: 80 }))
expect(wallpaperSampling.at(-1)).toBe(0) expect(wallpaperSampling.value).toBe(0)
expect(document.documentElement.dataset.glassScrollPresentation).toBe('native') expect(document.documentElement.dataset.glassScrollPresentation).toBe('native')
scrollY = 240 scrollY = 240
@@ -3823,13 +3806,152 @@ describe('glass optical surface discovery', () => {
scheduledCallbacks.forEach(callback => callback(performance.now() + 100 + pass * 16)) scheduledCallbacks.forEach(callback => callback(performance.now() + 100 + pass * 16))
} }
expect(wallpaperSampling.at(-1)).toBe(1) expect(wallpaperSampling.value).toBe(1)
expect(document.documentElement).not.toHaveAttribute('data-glass-scroll-presentation') expect(document.documentElement).not.toHaveAttribute('data-glass-scroll-presentation')
expect(callbacks.size).toBe(0) expect(callbacks.size).toBe(0)
scope.stop() scope.stop()
}) })
it('skips hidden native scroll draws and commits one frame before revealing the canvas', async () => {
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1200)
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800)
let scrollY = 0
vi.spyOn(window, 'scrollY', 'get').mockImplementation(() => scrollY)
const three = await import('three')
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
const callbacks = new Map<number, FrameRequestCallback>()
let frameId = 0
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(callback => {
frameId += 1
callbacks.set(frameId, callback)
return frameId
})
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(id => callbacks.delete(id))
appendOpticalSurface('app-hover-lift-card', { height: 300, width: 400, x: 40, y: 120 })
const scope = effectScope()
const renderer = scope.run(() =>
useGlassOpticalRenderer({
active: ref(true),
appearance: ref('clear'),
canvas: ref(document.createElement('canvas')),
dynamicsMode: ref<'fluid' | 'off' | 'ripple'>('off'),
quality: ref('high'),
routeKey: ref('/dashboard'),
surfaceSpace: 'scroll',
tintColor: ref('#8D51F9'),
wallpaperUrl: ref('/api/v1/login/wallpapers/opaque-id'),
}),
)
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
expect(document.documentElement.dataset.glassRendererState).toBe('ready')
const mainScene = render.mock.calls
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
.find(scene => scene.children[0]?.material?.uniforms.uScrollOffset)
if (!mainScene) throw new Error('main optical scene was not rendered')
const uniforms = mainScene.children[0].material!.uniforms
const framesBeforeNativeScroll = renderer?.renderedFrames.value
let presentationAtRestoreRender: string | undefined
render.mockImplementation(() => {
presentationAtRestoreRender = document.documentElement.dataset.glassScrollPresentation
})
render.mockClear()
window.dispatchEvent(new WheelEvent('wheel', { deltaY: 80 }))
expect(render).not.toHaveBeenCalled()
expect(renderer?.renderedFrames.value).toBe(framesBeforeNativeScroll)
expect(document.documentElement.dataset.glassScrollPresentation).toBe('native')
scrollY = 240
window.dispatchEvent(new Event('scroll'))
const firstScrollFrame = [...callbacks.values()][0]
callbacks.clear()
firstScrollFrame?.(performance.now() + 100)
expect(render).not.toHaveBeenCalled()
expect(uniforms.uScrollOffset.value.y).toBe(240)
expect(document.documentElement.dataset.glassScrollPresentation).toBe('native')
const secondScrollFrame = [...callbacks.values()][0]
callbacks.clear()
secondScrollFrame?.(performance.now() + 116)
expect(render).not.toHaveBeenCalled()
expect(document.documentElement.dataset.glassScrollPresentation).toBe('native')
const restoreFrame = [...callbacks.values()][0]
callbacks.clear()
restoreFrame?.(performance.now() + 132)
expect(render).toHaveBeenCalledTimes(1)
expect(presentationAtRestoreRender).toBe('native')
expect(renderer?.renderedFrames.value).toBe((framesBeforeNativeScroll ?? 0) + 1)
expect(document.documentElement).not.toHaveAttribute('data-glass-scroll-presentation')
scope.stop()
})
it.each(['loading', 'fallback'] as const)(
'keeps drawing while the scroll context is ready but the composite renderer state is %s',
async rootState => {
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1200)
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800)
const three = await import('three')
const render = vi.spyOn(three.WebGLRenderer.prototype, 'render')
const callbacks = new Map<number, FrameRequestCallback>()
let frameId = 0
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(callback => {
frameId += 1
callbacks.set(frameId, callback)
return frameId
})
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(id => callbacks.delete(id))
appendOpticalSurface('app-hover-lift-card', { height: 300, width: 400, x: 40, y: 120 })
const scope = effectScope()
const renderer = scope.run(() =>
useGlassOpticalRenderer({
active: ref(true),
appearance: ref('clear'),
canvas: ref(document.createElement('canvas')),
dynamicsMode: ref<'fluid' | 'off' | 'ripple'>('off'),
quality: ref('high'),
routeKey: ref('/dashboard'),
surfaceSpace: 'scroll',
tintColor: ref('#8D51F9'),
wallpaperUrl: ref('/api/v1/login/wallpapers/opaque-id'),
}),
)
await vi.waitFor(() => expect(renderer?.state.value).toBe('ready'))
for (let pass = 0; pass < 4 && callbacks.size > 0; pass += 1) {
const scheduledCallbacks = [...callbacks.values()]
callbacks.clear()
scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16))
}
render.mockClear()
window.dispatchEvent(new WheelEvent('wheel', { deltaY: 80 }))
expect(document.documentElement.dataset.glassScrollPresentation).toBe('native')
expect(render).not.toHaveBeenCalled()
document.documentElement.dataset.glassRendererState = rootState
window.dispatchEvent(new Event('scroll'))
const firstScrollFrame = [...callbacks.values()][0]
callbacks.clear()
firstScrollFrame?.(performance.now() + 100)
expect(renderer?.state.value).toBe('ready')
expect(document.documentElement.dataset.glassRendererState).toBe(rootState)
expect(render).toHaveBeenCalledTimes(1)
scope.stop()
},
)
it('clears ripple state before native scroll presentation takes ownership', async () => { it('clears ripple state before native scroll presentation takes ownership', async () => {
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1200) vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1200)
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800) vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800)
@@ -4120,6 +4242,14 @@ describe('glass optical surface discovery', () => {
callbacks.clear() callbacks.clear()
scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16)) scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16))
} }
const mainScene = render.mock.calls
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
.find(scene => scene.children[0]?.material?.uniforms.uRects)
if (!mainScene) throw new Error('main optical scene was not rendered')
const uniforms = mainScene.children[0].material!.uniforms as unknown as {
uRects: { value: Array<{ y: number }> }
}
const initialRectY = uniforms.uRects.value[0]?.y
render.mockClear() render.mockClear()
setRenderTarget.mockClear() setRenderTarget.mockClear()
@@ -4135,7 +4265,8 @@ describe('glass optical surface discovery', () => {
callbacks.clear() callbacks.clear()
scheduledCallbacks.forEach(callback => callback(performance.now() + 100)) scheduledCallbacks.forEach(callback => callback(performance.now() + 100))
expect(render).toHaveBeenCalled() expect(render).not.toHaveBeenCalled()
expect(uniforms.uRects.value[0]?.y).not.toBe(initialRectY)
expect(setRenderTarget).not.toHaveBeenCalled() expect(setRenderTarget).not.toHaveBeenCalled()
const transitionEnd = new Event('transitionend', { bubbles: true }) as TransitionEvent const transitionEnd = new Event('transitionend', { bubbles: true }) as TransitionEvent
@@ -4199,6 +4330,13 @@ describe('glass optical surface discovery', () => {
scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16)) scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16))
} }
expect(callbacks.size).toBe(0) expect(callbacks.size).toBe(0)
const mainScene = render.mock.calls
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
.find(scene => scene.children[0]?.material?.uniforms.uRects)
if (!mainScene) throw new Error('main optical scene was not rendered')
const scrollUniforms = mainScene.children[0].material!.uniforms as unknown as {
uRects: { value: Array<{ y: number }> }
}
querySelectorAll.mockClear() querySelectorAll.mockClear()
scrollY = 1000 scrollY = 1000
@@ -4209,16 +4347,7 @@ describe('glass optical surface discovery', () => {
const firstScrollFrame = [...callbacks.values()] const firstScrollFrame = [...callbacks.values()]
callbacks.clear() callbacks.clear()
firstScrollFrame.forEach(callback => callback(performance.now() + 100)) firstScrollFrame.forEach(callback => callback(performance.now() + 100))
const firstScrollScene = render.mock.calls.at(-1)?.[0] as unknown as { expect(scrollUniforms.uRects.value[0].y).toBeCloseTo(1 - (1100 + 240) / 2000)
children: Array<{
material: {
uniforms: {
uRects: { value: Array<{ y: number }> }
}
}
}>
}
expect(firstScrollScene.children[0].material.uniforms.uRects.value[0].y).toBeCloseTo(1 - (1100 + 240) / 2000)
expect(querySelectorAll).not.toHaveBeenCalled() expect(querySelectorAll).not.toHaveBeenCalled()
for (let pass = 0; pass < 3 && callbacks.size > 0; pass += 1) { for (let pass = 0; pass < 3 && callbacks.size > 0; pass += 1) {
@@ -4303,6 +4432,13 @@ describe('glass optical surface discovery', () => {
scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16)) scheduledCallbacks.forEach(callback => callback(performance.now() + pass * 16))
} }
expect(callbacks.size).toBe(0) expect(callbacks.size).toBe(0)
const mainScene = render.mock.calls
.map(call => call[0] as unknown as { children: Array<{ material?: ShaderMaterial }> })
.find(scene => scene.children[0]?.material?.uniforms.uRects)
if (!mainScene) throw new Error('main optical scene was not rendered')
const uniforms = mainScene.children[0].material!.uniforms as unknown as {
uRects: { value: Array<{ y: number }> }
}
querySelectorAll.mockClear() querySelectorAll.mockClear()
setOpticalSurfaceBounds(surface, { height: 300, width: 400, x: 40, y: 180 }) setOpticalSurfaceBounds(surface, { height: 300, width: 400, x: 40, y: 180 })
@@ -4311,17 +4447,7 @@ describe('glass optical surface discovery', () => {
const firstScrollFrame = [...callbacks.values()] const firstScrollFrame = [...callbacks.values()]
callbacks.clear() callbacks.clear()
firstScrollFrame.forEach(callback => callback(performance.now() + 100)) firstScrollFrame.forEach(callback => callback(performance.now() + 100))
expect(uniforms.uRects.value[0].y).toBeCloseTo(1 - (180 + 300) / 1200)
const firstScrollScene = render.mock.calls.at(-1)?.[0] as unknown as {
children: Array<{
material: {
uniforms: {
uRects: { value: Array<{ y: number }> }
}
}
}>
}
expect(firstScrollScene.children[0].material.uniforms.uRects.value[0].y).toBeCloseTo(1 - (180 + 300) / 1200)
expect(querySelectorAll).toHaveBeenCalled() expect(querySelectorAll).toHaveBeenCalled()
scope.stop() scope.stop()
@@ -1827,6 +1827,17 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
if (fluidDynamics && advanceFlow) { if (fluidDynamics && advanceFlow) {
resources.uniforms.uFlowTexture.value = fluidDynamics.step() resources.uniforms.uFlowTexture.value = fluidDynamics.step()
} }
// 原生 backdrop 接管期间 scroll canvas 已由 CSS 隐藏;保留事务、流场和 uniform 同步,跳过主材质输出。
if (
presentationSpace === 'scroll' &&
scrollWallpaperSamplingSuppressed &&
state.value === 'ready' &&
document.documentElement.dataset.glassRendererState === 'ready' &&
document.documentElement.dataset.glassScrollPresentation === 'native'
) {
return
}
if (presentationSpace === 'scroll') { if (presentationSpace === 'scroll') {
const { height: presentationHeight } = getCommittedPresentationSize() const { height: presentationHeight } = getCommittedPresentationSize()
const scaleY = presentationBufferHeight / Math.max(presentationHeight, 1) const scaleY = presentationBufferHeight / Math.max(presentationHeight, 1)