Производительность
Оверлей работает на одном компьютере с игрой, а игре важен каждый кадр. Несколько простых правил сделают оверлей плавным и почти незаметным для производительности.
Читайте только нужные данные
Каждый блок данных, к которому обращается код, приходит со своей частотой. Блок car приходит около 60 раз в секунду. Если оверлей погоды случайно обратится к car, он будет получать 60 лишних обновлений в секунду. Обращайтесь к блокам там, где они действительно нужны.
Рисуйте по кадрам экрана
Данные приходят со своей частотой и не идеально ровно, а экран обновляется со своей. Всё, что движется (стрелки, полосы, графики), рисуйте один раз за кадр экрана по последнему значению. Сообщения пусть только обновляют данные:
const frame = (): void => {
needle.style.transform = `rotate(${overlay.telemetry.car.rpm / maxRpm}turn)`;
requestAnimationFrame(frame);
};
overlay.on('ready', () => requestAnimationFrame(frame));Текст, который меняется несколько раз в секунду (времена кругов, позиции), можно обновлять прямо в обработчике telemetry: рисовать его каждый кадр незачем.
Плавные графики и карты
Данные приходят немного неравномерно, и если рисовать график по приходу сообщений, он дёргается. Чтобы этого не было, используйте overlay.sampledAt[блок] — момент, когда игра сняла эти данные. Время указано по тем же часам, что и у requestAnimationFrame (performance.now()).
Сохраняйте значения вместе с этим временем и рисуйте график с небольшой задержкой, около 50 мс. Тогда для каждого кадра есть значение до и после нужного момента, и между ними можно плавно интерполировать:
type Sample = { at: number; throttle: number };
const samples: Sample[] = [];
overlay.on('telemetry', ({ car }) => {
const at = overlay.sampledAt.car;
// Событие приходит при обновлении любого блока; новое значение car сохраняем один раз.
if (at === undefined || at === samples.at(-1)?.at) return;
samples.push({ at, throttle: car.throttle });
while ((samples[0]?.at ?? at) < at - 10_000) samples.shift(); // храним последние 10 с
});
const frame = (now: number): void => {
drawTrace(samples, now - 50); // рисуем на 50 мс в прошлом
requestAnimationFrame(frame);
};
requestAnimationFrame(frame);React
useOverlay из шаблона перерисовывает App при каждом сообщении каждого блока, который вы читаете. Для текста это нормально, а для стрелки или графика 60 раз в секунду — слишком много работы. Быстро меняющиеся части рисуйте в обход состояния React:
Без React страница и так рисуется вручную: достаточно кода выше.
export function Needle({ overlay, maxRpm }: NeedleProps) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
let id = requestAnimationFrame(function frame() {
const turn = overlay.telemetry.car.rpm / maxRpm;
if (ref.current) ref.current.style.transform = `rotate(${turn}turn)`;
id = requestAnimationFrame(frame);
});
return () => cancelAnimationFrame(id);
}, [overlay, maxRpm]);
return <div className="needle" ref={ref} />;
}Остальное разбейте на компоненты так, чтобы частые данные перерисовывали только тот компонент, который их показывает. Передавайте каждому компоненту только нужные ему значения.
Что ещё ускоряет отрисовку
- Двигайте и масштабируйте элементы через
transformиopacity. Если каждый кадр менятьwidth,topили шрифт, браузеру приходится заново раскладывать всю страницу. - Меняйте текст и атрибуты уже существующих элементов, а не создавайте списки заново при каждом сообщении. Если строки гонщиков привязаны к
carIdx, они остаются на месте, когда меняется порядок. - Для постоянно меняющихся чисел задайте
font-variant-numeric: tabular-nums: все цифры станут одной ширины, и текст не будет прыгать. - Много точек или линий (карта, длинный график) рисуйте на canvas, а не сотнями элементов.