Date.now может убить твой RPS
Отвлечемся на более прикладную тему – микрооптимизацию. Уточню сразу, для 99% систем, это не нужно, не те нагрузки, не тот RPS, влияние микроскопическое. Но если у тебя на hot-path уже набегает до 100k RPS и каждый тик на счету, об этом уже можно подумать.
Вот возьмем совершенно с виду безобидный Date.now(), статический метод возвращает миллисекунды с 1970-01-01T00:00:00Z. Используют его повсеместно для метрик, вычислений, логов.
«Ну читает таймер и читает, что тут такого?» - для обычной системы в целом ничего. Для нагруженной — это лишний системный вызов (или почти системный), прямо внутри горячего цикла.
Как это работает? Date.now не просто читает регистр или "переменную", он читает системное время.
Под капотом: 1. JS → V8 intrinsic → C++ JSDate::CurrentTimeValue(isolate). 2. Системный вызов: - Linux: clock_gettime(CLOCK_REALTIME) - Windows: GetSystemTimeAsFileTime() 3. Перевод в миллисекунды и возврат в JS.
Вызов идёт синхронно. Eventloop не задействуется. Date.now() не кеширует значения и каждый раз ходит в системный API. Монотонность не гарантируется, системное время может корректироваться (например, через NTP).
Цена вопроса (примерный порядок):
- vDSO (
clock_gettimeбез перехода в ядро): 30–80 нс. - С переходом в ядро (syscall): 300–500 нс.
- На 100k RPS × 3 вызова на запрос = 300k вызовов/с.
- При 0.3 мкс на вызов — ~90 мс CPU/с (около 10% одного ядра) только на чтение времени.
А как проверял? Стенд предельно прямолинейный, без зависимостей. server.mjs — два ендпоинта, разница только в том, где вызывается Date.now(). Внутри горячего цикла или снаружи и цикл работает с локальной переменной. bench.mjs — шлёт запросы параллельно, считает RPS и перцентили p50/p95/p99 по длительности ответа последовательно для каждого ендпоинта.
Параметры запуска: DURATION=10s, CONCURRENCY=32, LOOPS=200_000
/bad — Date.now() в цикле. RPS - 219 p50 - 143 p95 - 149 p99 - 290
/good — один вызов и работа с локальной переменной. RPS - 8023 p50 - 3.9 p95 - 4.0 p99 - 7.7
Это не «нанопроценты». Это порядок разницы. Просто перестали дёргать Date.now() миллионы раз в горячем цикле.
А какая альтернатива? И когда заменять? Оговорюсь, не стоит бросаться и выжигать Date.now повсеместно. Стоит учитывать контекст.
— Хочешь измерять интервалы — используй монотонные таймеры: performance.now() (мс с долями, монотонный) или process.hrtime.bigint() (нс). Они не завязаны на системные поправки времени и обычно дешевле. — Нужен штамп времени (для логов/бизнес-логики) — ок, Date.now(). Но один раз на запрос/батч, а не в каждой итерации. — В горячем коде — вынеси чтение времени из цикла, работай с локальной переменной. Это дёшево и надёжно.
Что в итоге? Date.now() — нормальный инструмент. Но в hot‑path многократные вызовы — это может быть overhead, который влечет за собой жирные p95/p99 и просадку RPS.
Оптимизация тривиальна: читаешь один раз или переключаешься на монотонный таймер там, где нужно мерить интервалы. Никаких фреймворков, никакой магии.