Racelink
Документация · Производительность

// Документация · Продвинутое

Производительность

Оверлей работает на одном компьютере с игрой, а игре важен каждый кадр. Несколько простых правил сделают оверлей плавным и почти незаметным для производительности.

Читайте только нужные данные

Каждый блок данных, к которому обращается код, приходит со своей частотой. Блок car приходит около 60 раз в секунду. Если оверлей погоды случайно обратится к car, он будет получать 60 лишних обновлений в секунду. Обращайтесь к блокам там, где они действительно нужны.

Рисуйте по кадрам экрана

Данные приходят со своей частотой и не идеально ровно, а экран обновляется со своей. Всё, что движется (стрелки, полосы, графики), рисуйте один раз за кадр экрана по последнему значению. Сообщения пусть только обновляют данные:

src/main.ts
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 мс. Тогда для каждого кадра есть значение до и после нужного момента, и между ними можно плавно интерполировать:

src/trace.ts
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 страница и так рисуется вручную: достаточно кода выше.

src/Needle.tsx
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, а не сотнями элементов.