perf: render timer less frequently
This commit is contained in:
parent
bf26d3e80a
commit
6e6cf2c4f8
3 changed files with 103 additions and 15 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,5 +1,7 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
.claude
|
||||||
|
|
||||||
*.*~
|
*.*~
|
||||||
*.*swp
|
*.*swp
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||||
import { TimerService } from '../utils/timer/timer.js'
|
import { TimerService } from '../utils/timer/timer.js'
|
||||||
|
|
||||||
describe('TimerService.pretty', () => {
|
describe('TimerService.pretty', () => {
|
||||||
|
|
@ -31,3 +31,65 @@ describe('TimerService.clean', () => {
|
||||||
expect(timer.clean('05', ':')).toBe('05:')
|
expect(timer.clean('05', ':')).toBe('05:')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('TimerService rendering', () => {
|
||||||
|
let display
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
display = document.createElement('span')
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows 0 before the timer starts', () => {
|
||||||
|
const timer = new TimerService()
|
||||||
|
timer.initialize(display)
|
||||||
|
expect(display.innerHTML).toBe('0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('drives updates with requestAnimationFrame, not a 1ms interval', () => {
|
||||||
|
const raf = vi.spyOn(window, 'requestAnimationFrame')
|
||||||
|
const interval = vi.spyOn(window, 'setInterval')
|
||||||
|
|
||||||
|
const timer = new TimerService()
|
||||||
|
timer.initialize(display)
|
||||||
|
timer.start()
|
||||||
|
|
||||||
|
expect(raf).toHaveBeenCalled()
|
||||||
|
expect(interval).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('only touches the DOM when the displayed value changes', () => {
|
||||||
|
const timer = new TimerService()
|
||||||
|
timer.initialize(display)
|
||||||
|
timer.start()
|
||||||
|
|
||||||
|
let writes = 0
|
||||||
|
const span = { set innerHTML(_v) { writes++ } }
|
||||||
|
timer.display = span
|
||||||
|
|
||||||
|
// Same tenth-of-a-second value across frames -> a single DOM write.
|
||||||
|
timer.time = 1500
|
||||||
|
timer.render()
|
||||||
|
timer.render()
|
||||||
|
timer.render()
|
||||||
|
expect(writes).toBe(1)
|
||||||
|
|
||||||
|
// A new value -> one more write.
|
||||||
|
timer.time = 1600
|
||||||
|
timer.render()
|
||||||
|
expect(writes).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the elapsed time in milliseconds from stop()', () => {
|
||||||
|
const timer = new TimerService()
|
||||||
|
timer.initialize(display)
|
||||||
|
timer.start()
|
||||||
|
vi.advanceTimersByTime(2500)
|
||||||
|
timer.tick()
|
||||||
|
expect(timer.stop()).toBe(2500)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { LoggerService } from '../logger/logger'
|
import { LoggerService } from '../logger/logger'
|
||||||
|
|
||||||
const INTERVAL = 1
|
|
||||||
|
|
||||||
export class TimerService {
|
export class TimerService {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.loggerService = new LoggerService()
|
this.loggerService = new LoggerService()
|
||||||
|
this.time = 0
|
||||||
|
this.rendered = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize(el) {
|
initialize(el) {
|
||||||
|
|
@ -13,33 +13,58 @@ export class TimerService {
|
||||||
|
|
||||||
this.display = el
|
this.display = el
|
||||||
this.startTime = undefined
|
this.startTime = undefined
|
||||||
if (this.id) {
|
if (this.id !== undefined) {
|
||||||
this.stop()
|
this.stop()
|
||||||
}
|
}
|
||||||
this.updateDisplay()
|
this.time = 0
|
||||||
|
this.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (this.running || !this.display) return
|
if (this.running || !this.display) return
|
||||||
|
|
||||||
this.running = true
|
this.running = true
|
||||||
this.startTime = new Date().getTime()
|
this.startTime = Date.now()
|
||||||
this.id = window.setInterval(() => this.updateDisplay(), INTERVAL)
|
this.tick()
|
||||||
this.loggerService.debug(`started timer id: ${this.id}`)
|
this.loggerService.debug('started timer')
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
this.running = false
|
this.running = false
|
||||||
clearInterval(this.id)
|
if (this.id !== undefined) {
|
||||||
this.loggerService.debug(`stopped timer id: ${this.id}`)
|
window.cancelAnimationFrame(this.id)
|
||||||
|
}
|
||||||
this.id = undefined
|
this.id = undefined
|
||||||
|
this.loggerService.debug('stopped timer')
|
||||||
return this.time
|
return this.time
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDisplay() {
|
/**
|
||||||
let currentTime = new Date().getTime() - this.startTime
|
* Recompute the elapsed time and schedule the next frame.
|
||||||
this.time = Math.floor(currentTime / INTERVAL)
|
* Driven by requestAnimationFrame so it aligns with the browser's paint
|
||||||
this.display.innerHTML = this.pretty(this.time) || '0'
|
* cadence and pauses automatically when the tab is hidden — instead of the
|
||||||
|
* old fixed 1ms interval that fired ~1000 times a second.
|
||||||
|
*/
|
||||||
|
tick() {
|
||||||
|
this.time = Date.now() - this.startTime
|
||||||
|
this.render()
|
||||||
|
if (this.running) {
|
||||||
|
this.id = window.requestAnimationFrame(() => this.tick())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write to the DOM only when the visible value actually changes. The display
|
||||||
|
* has 100ms (tenths-of-a-second) resolution, so most frames are a no-op and
|
||||||
|
* cost no reflow.
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
if (!this.display) return
|
||||||
|
const text = this.pretty(this.time) || '0'
|
||||||
|
if (text !== this.rendered) {
|
||||||
|
this.display.innerHTML = text
|
||||||
|
this.rendered = text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pretty(duration) {
|
pretty(duration) {
|
||||||
|
|
@ -60,4 +85,3 @@ export class TimerService {
|
||||||
return (str === '00') ? '' : `${str}${separator}`
|
return (str === '00') ? '' : `${str}${separator}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue