The concept involves tracking FPS within the rendering loop (by measuring intervals between requestAnimationFrame
calls) and adjusting DPR (Device Pixel Ratio) accordingly.
When we say "tracking", it means storing these intervals in an array, filtering out extreme values to prevent spikes, calculating an average, and comparing it against predefined thresholds.
const highFrequencyThreshold = 20; // ~50 FPS
const lowFrequencyThreshold = 34; // ~30 FPS
const minDpr = 0.5;
const maxDpr = window.devicePixelRatio;
const deltaDpr = 0.1;
const relaxPeriod = 4000;
const accumulatorLength = 20;
const frameTimestamp = performance.now();
const frequencyAccumulator = [];
const lastUpdatedAt = null;
const renderer = new WebGLRenderer({
antialias: true,
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
animate();
function animate(timestamp: number = performance.now()) {
requestAnimationFrame(animate);
monitor(frameTimestamp, timestamp);
frameTimestamp = timestamp;
// additional animation calculations and rendering
// ...
}
function monitor(frameTimestamp: number, now: number) {
collectFrequency(now - frameTimestamp);
// if the accumulator is not yet full
if (frequencyAccumulator.length < accumulatorLength) {
return;
}
// a recent update has occurred
if (now - lastUpdatedAt < relaxPeriod) {
return;
}
const dpr = renderer.getPixelRatio();
const frequencyMedian = median(...frequencyAccumulator);
if (frequencyMedian > lowFrequencyThreshold && dpr > minDpr) {
updateDpr(dpr, -deltaDpr, now);
} else if (frequencyMedian < highFrequencyThreshold && dpr < maxDpr) {
updateDpr(dpr, deltaDpr, now);
}
}
function collectFrequency(frequency: number) {
if (frequency > 0) {
frequencyAccumulator.push(frequency);
}
if (frequencyAccumulator.length > accumulatorLength) {
frequencyAccumulator.shift();
}
}
function updateDpr(dpr: number, delta: number, now: number) {
renderer.setPixelRatio(dpr + delta);
frequencyAccumulator = [];
lastUpdatedAt = now;
}
function median(...elements: number[]): number {
const indexOfMin = elements.indexOf(Math.min(...elements));
const indexOfMax = elements.indexOf(Math.max(...elements));
const noMinMax = elements.filter((_, index) => index !== indexOfMin && index !== indexOfMax);
return average(...noMinMax);
}
function average(...elements: number[]): number {
return elements.reduce((sum, value) => sum + value, 0) / elements.length;
}
Keep in mind that adjusting the DPR can lead to temporary animation stuttering.
Additionally, a more intelligent method could be implemented for fine-tuning the DPR value instead of using a linear step of 0.1
, such as employing a bisectional search technique.