From 610574c23ae44f95b7b0326e4456e17a6a68a2a0 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Sun, 13 Nov 2022 17:05:44 +0400 Subject: [PATCH] [Perf] Fix slow `animate` function --- src/util/FrameDebugger.ts | 99 ++++++++++++++++++++++++++ src/util/animation.ts | 12 +++- src/util/fastSmoothScrollHorizontal.ts | 27 ++++--- 3 files changed, 121 insertions(+), 17 deletions(-) create mode 100644 src/util/FrameDebugger.ts diff --git a/src/util/FrameDebugger.ts b/src/util/FrameDebugger.ts new file mode 100644 index 000000000..fac14a617 --- /dev/null +++ b/src/util/FrameDebugger.ts @@ -0,0 +1,99 @@ +const RANDOM = 0.95; +const DEBOUNCE = 3000; + +export default class FrameDebugger { + private durations: number[] = []; + + private startedAtByFrameKey: Record = {}; + + private passedFrames: string[] = []; + + private timeout: number | undefined; + + constructor(private name: string = '[No name]') { + } + + onFrameStart(frameKey = '0') { + if (this.passedFrames.includes(frameKey)) { + // debugger + } + + if (this.startedAtByFrameKey[frameKey]) { + return; + } + + this.startedAtByFrameKey[frameKey] = performance.now(); + } + + onFrameEnd(frameKey = '0', onAnimationEnd?: AnyToVoidFunction) { + if (!this.startedAtByFrameKey[frameKey]) { + return; + } + + const duration = performance.now() - this.startedAtByFrameKey[frameKey]!; + + if (this.passedFrames.includes(frameKey)) { + // debugger + } + + this.passedFrames.push(frameKey); + this.durations.push(duration); + + this.startedAtByFrameKey[frameKey] = undefined; + + if (Math.random() < RANDOM) { + return; + } + + if (this.timeout) { + clearTimeout(this.timeout); + } + + // eslint-disable-next-line no-restricted-globals + this.timeout = self.setTimeout(() => { + if (!this.durations.length) { + return; + } + + const max = Math.max(...this.durations); + const min = Math.max(...this.durations); + const maxIndex = this.durations.indexOf(max); + const minIndex = this.durations.indexOf(min); + const reduced = this.durations.slice(); + reduced.splice(maxIndex, 1); + reduced.splice(minIndex, 1); + const avg = reduced.reduce((acc, cur) => acc + cur, 0) / this.durations.length; + + // eslint-disable-next-line no-console + console.log( + '!!!', + this.name, + 'total frames:', + this.durations.length, + ', avg duration:', + avg.toFixed(2), + ', max duration:', + Math.max(...reduced).toFixed(2), + ', min duration:', + Math.min(...reduced).toFixed(2), + ); + + onAnimationEnd?.(); + + this.reset(); + }, DEBOUNCE); + } + + reset() { + this.durations = []; + + this.startedAtByFrameKey = {}; + + this.passedFrames = []; + + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = undefined; + } + } +} diff --git a/src/util/animation.ts b/src/util/animation.ts index c7c21d9b2..2889e360e 100644 --- a/src/util/animation.ts +++ b/src/util/animation.ts @@ -24,9 +24,17 @@ export function animateSingle(tick: Function, instance?: AnimationInstance) { } export function animate(tick: Function) { + fastRaf(() => { + if (tick()) { + animate(tick); + } + }); +} + +export function animateInstantly(tick: Function) { if (tick()) { fastRaf(() => { - animate(tick); + animateInstantly(tick); }); } } @@ -72,7 +80,7 @@ export function animateNumber({ const t0 = Date.now(); let canceled = false; - animate(() => { + animateInstantly(() => { if (canceled) return false; const t1 = Date.now(); let t = (t1 - t0) / duration; diff --git a/src/util/fastSmoothScrollHorizontal.ts b/src/util/fastSmoothScrollHorizontal.ts index 0d7c5c210..30ae9d5db 100644 --- a/src/util/fastSmoothScrollHorizontal.ts +++ b/src/util/fastSmoothScrollHorizontal.ts @@ -2,7 +2,6 @@ import { getGlobal } from '../global'; import { ANIMATION_LEVEL_MIN } from '../config'; import { animate } from './animation'; -import { fastRaf } from './schedulers'; const DEFAULT_DURATION = 300; @@ -62,23 +61,21 @@ function scrollWithJs(container: HTMLElement, left: number, duration: number) { }); const startAt = Date.now(); - fastRaf(() => { - animate(() => { - if (isStopped) return false; + animate(() => { + if (isStopped) return false; - const t = Math.min((Date.now() - startAt) / duration, 1); + const t = Math.min((Date.now() - startAt) / duration, 1); - const currentPath = path * (1 - transition(t)); - container.scrollLeft = Math.round(target - currentPath); + const currentPath = path * (1 - transition(t)); + container.scrollLeft = Math.round(target - currentPath); - if (t >= 1) { - container.style.scrollSnapType = ''; - container.dataset.scrollId = undefined; - stopById.delete(id); - resolve(); - } - return t < 1; - }); + if (t >= 1) { + container.style.scrollSnapType = ''; + container.dataset.scrollId = undefined; + stopById.delete(id); + resolve(); + } + return t < 1; }); return promise;