Temporal: долгий процесс решения проблемы времени в JavaScript
- вторник, 17 марта 2026 г. в 00:00:05
JavaScript уникален тем, что работает во всех браузерах. У него нет какого-то одного «владельца», поэтому нельзя внести изолированное изменение и ждать, что оно будет применено везде. Эволюция происходит через TC39 — Технический комитет, отвечающий за ECMAScript.

Предложения должны пройти последовательность этапов развития:
Этап 0: идея.
Этап 1: принято пространство задач.
Этап 2: выбрана архитектура драфта, но работа продолжится.
Этап 2.7: предложение одобрено в принципе; ожидает тестирования и обратной связи.
Этап 3: реализация и обратная связь.
Этап 4: предложение стандартизовано.
В 2018 году, когда я впервые изучал Temporal, он находился на Этапе 1. Комитет TC39 был убеждён, что проблема реальна. Это было радикальное предложение по добавлению в JavaScript новой библиотеки дат и времени. Она должна была:
Стать заменой Date.
Добавить новые типы DateTime (вместо единого API).
Стать неизменяемой.
Добавить поддержку часовых поясов и календаря.
Но как мы к этому пришли? Почему Date вызывает столько проблем? Чтобы ответить на эти вопросы, нужно вернуться назад.
В 1995 году Брендану Айку поручили за 10-дневный спринт создать Mocha (который позже станет JavaScript). В условиях жёсткого дефицита времени ему пришлось принимать многие решения, исходя из прагматичных соображений. Одно из них заключалось в прямом портировании реализации Date из языка Java. Брендан позже объяснял:
Это был прямой порт кода Date языка Java, переписанный Кеном Смитом с Java на C (единственный в Mocha код, который писал не я).
На тот момент это имело смысл. Популярность Java росла, а JavaScript позиционировали, как его легковесного компаньона. Разработчики даже формулировали его философию, как MILLJ: Make It Look Like Java (Пусть будет как Java).

Брендан также отметил, что менять API было бы сложно политически:
Изменения вызвали бы запутанность и баги, ведь все ожидали, что Java станет его «старшим братом»; Sun бы тоже возражала.
В тот момент важнее была согласованность с Java, чем фундаментальное переосмысление модели времени. Это стало прагматичным компромиссом. Веб был молод, а большинство приложений на JavaScript оставались простыми, по крайней мере, поначалу.
К 2010-м JavaScript уже применялся в банковских системах, торговых терминалах, инструментах для совместной работы и в других сложных системах, выполняющихся во всех часовых поясах планеты. Для разработчиков Date становился всё более проблемным.
Разработчики часто писали вспомогательные функции, случайно меняющие исходный объект Date вместо возвращения нового:
const date = new Date("2026-02-25T00:00:00Z"); console.log(date.toISOString()); // "2026-02-25T00:00:00.000Z" function addOneDay(d) { // Ой! Здесь мы изменяем дату d.setDate(d.getDate() + 1); return d; } addOneDay(date); console.log(date.toISOString()); // "2026-02-26T00:00:00.000Z"
const billingDate = new Date("Sat Jan 31 2026"); billingDate.setMonth(billingDate.getMonth() + 1); // Ожидается: Feb 28 // На самом деле: Mar 02
Иногда нам нужно бывает получить последний день месяца, и мы попадаем в подобные ловушки — месяц увеличивается на единицу, но дни остаются теми же. Date не преобразует недопустимые календарные результаты в правильные данные, а незаметно выполняет переполнение в следующий месяц.
new Date("2026-06-25 15:15:00").toISOString(); // Потенциальные возвращаемые значения: // - В локальном часовом поясе // - Invalid Date RangeError // - UTC
В этом примере строка похожа, но не полностью совпадает с ISO 8601. Раньше поведение браузеров в случае строк с «почти ISO» было не определено в спецификации. Некоторые браузеры обрабатывали их, как локальное время, другие — как UTC, а один вообще мог выбросить ошибку недопустимых входящих значений.
Подобных ситуаций очень много, поэтому последние три десятка лет Date был болевой точкой для разработчиков на JavaScript.

У экосистемы веба не было иных вариантов, кроме как патчить недостатки Date с помощью библиотек. На графике ниже показан рост скачиваний библиотек для работы с датами и временем. Сегодня их еженедельно загружают больше ста миллионов раз.
Флагманом изменений стал Moment.js, который мог похвастаться выразительным API, мощными возможностями парсинга и столь необходимой иммутабельностью. Эта библиотека, созданная в 2011 году, быстро стала стандартом де-факто для манипуляций с датами и временем в JavaScript. Так значит, проблема решена? Всем просто нужно скачать её и забыть о проблемах.
Широкое распространение moment.js (и других подобных библиотек) вызвало новые проблемы. При добавлении библиотеки увеличивался размер бандла из-за того, что его необходимо было выпускать с собственным множеством информации локали плюс данными о часовых поясах, взятыми из базы данных часовых поясов.
Несмотря на применение минификаторов, компиляторов и инструментов статического анализа, от всех этих дополнительных данных невозможно было избавиться, потому что большинство разработчиков не знало заранее, какие локали или часовые пояса им понадобятся. Чтобы подстраховаться, большинство разработчиков брало все данные целиком и выпускало их своим пользователям.


Мэгги Джонсон-Пинт, которая уже много лет работает мейнтейнером Moment.js, часто слышит просьбы сделать что-нибудь с размером пакета.
С moment мы попали в такую ситуацию: нам приходилось больше заниматься поддержкой, чтобы угнаться за модулями, webpack и людьми, которым нужна была неизменяемость из-за React, чем разрабатывать совершенно новую функциональность
И, разумеется, люди постоянно говорят о размере.
В 2017 году Мэгги решила, что настала пора для стандартизации дат и времени в «Temporal Proposal», предназначенного для плэнерного заседания TC39 того года. Предложение было встречено с огромным энтузиазмом, благодаря чему оно перешло на Этап 1.
Этап 1 стал серьёзной вехой, но до финиша было ещё далеко. После первоначального всплеска энергии прогресс, разумеется, замедлился. Мэгги и Мэтт Джонсон-Пинт, а также Брайан Тёрлсон руководили этим проектом, одновременно пытаясь справляться с другими своими обязанностями в Microsoft. Temporal находился ещё на ранних этапах развития, поэтому промежуточные результаты выглядели непрезентабельно: сбор требований, уточнение семантики и превращение «болевой точки экосистемы» в архитектуру, которую можно будет выпустить.
Для Bloomberg эти проблемы не были чем-то теоретическим.
В Terminal мы активно применяем JavaScript, используя такие среды исполнения и движки, как Chromium, Node.js и SpiderMonkey. Наши пользователи и финансовые рынки, в которые они инвестируют, находятся во всех часовых поясах мира. Мы постоянно передаём временные метки: между сервисами, в хранилище, в UI и между системами; все они должны иметь понимание о том, что такое «сейчас», даже когда государства почти без предупреждения меняют правила перехода на летнее время.
Кроме того, у нас есть требования, для выполнения которых встроенная модель Date попросту не предназначена:
Настраиваемый пользователями часовой пояс, отличающийся от часового пояса машины (к тому же он может меняться от запроса к запросу).
Корректная обработка часовых поясов с учётом истории на основе обновлений IANA Time Zone Database (tzdata).
Временные метки высокой точности (как минимум в наносекундах) без необходимости постоянного добавления новых полей в самописные обёртки.
Параллельно с тем, как Мэгги вносила предложение Temporal на рассмотрение TC39, разработчик Bloomberg Эндрю Папроцки вёл с Igalia переговоры о том, чтобы сделать часовые пояса настраиваемыми в V8. В частности, они обсуждали добавление поддерживаемого слоя косвенности, чтобы эмбеддер мог управлять часовым поясом, а не использовать выбранный в операционной системе по умолчанию. В этой беседе Дэниел Эренберг (работавший тогда в Igalia) рассказал Эндрю о первых разработках Temporal, потому что они выглядели поразительно похожими на уже имеющиеся у Bloomberg типы дат и времени.
Это обсуждение позволило навести первые мосты между потребностями Bloomberg в продакшене, опытом Igalia в браузерах и стандартах, а также зарождающимся вектором развития Temporal. В последующие годы Bloomberg совместно с Igalia вкладывал время своих разработчиков непосредственно в развитие Temporal, пока он не превратился в систему, которую могла бы использовать экосистема в целом. Эндрю искал в Bloomberg добровольцев, желавших развивать Temporal; героем разработки спецификации стал Филип Данкел. Вместе с Эндрю они убедили Bloomberg вложиться в реализацию Temporal, в том числе и активно взаимодействовать для этого с Igalia. Благодаря их поддержке Филип Чименто и Удждвал Шарма начали всё своё рабочее время посвящать развитию Temporal.
Позже к этой команде героев присоединился Шейн Карр, представляющий команду интернационализации Google. Он помогал нам в таких аспектах интернационализации, как, например, календари, а также был посредником между процессом стандартизации и мнениями пользователей, испытывавших трудности с инструментами, задействующими API интернационализации JavaScript API (Intl), например, с форматированием, часовыми поясами и календарями.
Кроме того, в 2020 году к команде героев добровольно присоединился Джастин Грант. За десять лет работы в трёх стартапах, которым требовалась обработка данных с временными метками, он был свидетелем того, команды разработчиков тратят тысячи часов на устранение ошибок с датами, временем и часовыми поясами. Опыт Джастина позволил нам основываться на реальных сценариях использования, помог предугадывать ошибки, которые могут совершать разработчики, и обеспечил выпуск в Temporal Temporal.ZonedDateTime API, благодаря которому баги перехода на летнее/зимнее время должны были остаться в прошлом.
Мэгги Джонсон-Пинт (Microsoft),
Мэтт Джонсон-Пинт (Microsoft),
Брайан Тёрлсон (Microsoft),
Ричард Гибсон (Agoric),
Филипп Данкел (Bloomberg),
Удждвал Шарма (Igalia),
Филип Чименто (Igalia),
Джейсон Уильямс (Bloomberg),
Шейн Карр (Google),
Джастин Грант (приглашённый эксперт).
Temporal — это объект пространства имён верхнего уровня (аналогично Math и Intl), существующий в глобальной области видимости. Под ним находятся «типы», существующие в виде конструкторов. Ожидается, что разработчики будут обращаться к нужному им типу через API, например, Temporal.PlainDateTime.
Вот, какие типы уже есть в Temporal:
Если вы не знаете, какой тип Temporal вам нужен, начните с Temporal.ZonedDateTime. Концептуально это самая близкая замена Date, но без его «выстрелов в ногу».
Date описывает:
Конкретный момент времени (внутри это миллисекунды от начала эпохи).
Интерпретируемый через текущий часовой пояс машины.
С косвенным изменяемым поведением.
Temporal.ZonedDateTime описывает:
Конкретный момент времени.
С явно указанным часовым поясом.
С явно указанным календарём.
И полной корректностью летнего времени.
Всё это в виде неизменяемого значения.
Если сейчас вы пишете такой код:
const now = new Date();
То его эквивалентом в Temporal будет такой:
const now = Temporal.Now.zonedDateTimeISO();
В примере выше используется пространство имён Now, дающее тип, уже привязанный к текущему локальному времени и часовому поясу.
Этот тип оптимизирован под DateTime, поэтому может потребовать арифметику дат и времени, при вычислениях которой потенциально могут возникать проблемы. ZonedDateTime может учитывать такие переходы при любых прибавлениях или вычитаниях времени (см. пример ниже).
// Начало летнего времени Лондона: 2026-03-29 01:00 -> 02:00 const zdt = Temporal.ZonedDateTime.from( "2026-03-29T00:30:00+00:00[Europe/London]", ); console.log(zdt.toString()); // → "2026-03-29T00:30:00+00:00[Europe/London]" const plus1h = zdt.add({ hours: 1 }); console.log(plus1h.toString()); // "2026-03-29T02:30:00+01:00[Europe/London]" (01:30 не существует)
В этом примере мы получаем не 01:30, а 02:30, потому что в этот конкретный момент времени 01:30 не существует.
Temporal.Instant — это конкретный момент времени, не имеющий часового пояса, перехода на летнее время и календаря. Он описывает время, прошедшее с полночи 1 января 1970 года (эпоха Unix). В отличие от Date, имеющего очень похожую модель данных, Instant измеряется не в миллисекундах, а в наносекундах. Это решение было принято героями, потому что несмотря на снижение точности в браузерах из соображений безопасности, разработчикам всё равно приходится иметь дело с временными метками наносекундной точности, которые можно генерировать из другого источника.
Типичный пример использования Temporal.Instant выглядит так:
// Один конкретный момент времени const instant = Temporal.Instant.from("2026-02-25T15:15:00Z"); instant.toString(); // "2026-02-25T15:15:00Z" instant.toZonedDateTimeISO("Europe/London").toString(); // "2026-02-25T15:15:00+00:00[Europe/London]" instant.toZonedDateTimeISO("America/New_York").toString(); // "2026-02-25T10:15:00-05:00[America/New_York]"
Instant можно создать и затем преобразовать в другой формат DateTime, учитывающий часовой пояс (подробнее об этом ниже). Скорее всего, разработчики будут хранить Instant и использовать разные преобразования часовых поясов для отображения того же времени пользователям в их часовых поясах.
Также у нас есть семейство простых типов. Их можно назвать «временем на часах»: подобно аналоговым часам на стене, они не проверяют переход на летнее время и часовые пояса. Это простое время (перевод стрелок вперёд на час добавит час, даже если это происходит во время перехода на летнее время).
У нас есть множество типов с постепенно уменьшающимся объёмом информации. Они полезны тем, что можно выбрать нужный тип и не беспокоиться о вычислениях с другими ненужными данными (например, с расчётом времени, если нужно отображать только дату).
Кроме того, эти типы полезны, если вы планируете лишь отображать значение пользователю и вам не нужно выполнять арифметику дат/времени, например, перемещаться вперёд или назад на недели (для этого потребуется календарь) или на часы (при этом возможно пересечение границы перехода на летнее или зимнее время). Ограничения части этих типов и обеспечивают их полезность. Разработчику будет сложно попасть в ловушку и столкнуться с неожиданными багами.
const date = Temporal.PlainDate.from({ year: 2026, month: 3, day: 11 }); // => 2026-03-11 date.year; // => 2026 date.inLeapYear; // => false date.toString(); // => '2026-03-11'
Temporal поддерживает календари. Браузеры и среды исполнения имеют встроенные календари, позволяющие представлять данные, отображать их и выполнять с ними вычисления в выбранной пользователем календарной системе, а не просто иначе форматировать дату григорианского календаря.
Так как объекты Temporal учитывать календари, операции вида «прибавить один месяц» выполняются по правилам конкретного календаря, поэтому результат оказывается ожидаемым. В примере ниже я прибавляю один месяц еврейского календаря к дате еврейского календаря:
const today = Temporal.PlainDate.from("2026-03-11[u-ca=hebrew]"); today.toLocaleString("en", { calendar: "hebrew" }); // '22 Adar 5786' const nextMonth = today.add({ months: 1 }); nextMonth.toLocaleString("en", { calendar: "hebrew" }); // '22 Nisan 5786'
При работе с устаревшим Date невозможно выразить операцию «прибавить один месяц еврейского календаря» в виде операции первого класса. Можно выполнить форматирование согласно другому календарю, но внутри всё равно будет использоваться арифметика григорианского месяца.
Если попробовать аппроксимировать это при помощи Date, то код может выглядеть так:
const legacyDate = new Date(2026, 2, 11); legacyDate.toLocaleDateString("en", { calendar: "hebrew" }); // '22 Adar 5786' legacyDate.setMonth(legacyDate.getMonth() + 1); legacyDate.toLocaleDateString("en", { calendar: "hebrew" }); // '24 Nisan 5786'
Происходит прибавление одного григорианского месяца (март → апрель). Когда мы затем отображаем результат в еврейском календаре, то оказываемся в другом дне, 24 нисана, а не 22 нисана, потому что структура и длительность месяцев в этих календарях различаются.
Наш последний тип — это Temporal.Duration. Duration описывает фиксированный промежуток времени; он может использоваться с любым из остальных типов при сложении и вычитании. Ещё одна полезная особенность Duration заключается в отображении его в разных единицах, например, как здесь:
const duration = Temporal.Duration.from({ hours: 130, minutes: 20, }); duration.total({ unit: "second" }); // => 469200
В большинстве библиотек дат и времени уже есть тип промежутков времени, поэтому было логично добавить его. Кроме того, он дополняет другие типы, позволяя разработчику сравнивать Time или DateTime, получая при этом тип Duration.
Реализовать Temporal было непросто. Это было очень крупное предложение, привносящее в JavaScript больше изменений, чем любое другое предложение за всю историю языка. Вот некоторые из трудностей:
Эта ОГРОМНАЯ спецификация больше, чем вся спецификация ECMA-402 (Спецификация интернационализации); из-за этого её сложно (но не невозможно) реализовать одному человеку.
Из-за изменчивости спецификации за ней было сложно угнаться. В процессе развития в спецификацию вносились изменения, поэтому реализации поспевали за ней с трудом.
Браузерам требуется, чтобы почти все аспекты были эффективными и обеспечивали высокую производительность.
Temporal — самое крупное добавление в ECMAScript со времён ES2015
Благодаря отличной работе Андре Баргулла Firefox был способен реализовывать Temporal в процессе уточнения спецификации, однако на ранних этапах не все браузеры и движки могли с ним работать. Из-за этого на последних стадиях Этапа 3 многим придётся догонять спецификацию.
Судя по количеству тестов, добавленных в официальный набор тестов ECMAScript (Test262), Temporal стал самым масштабным дополнением спецификации ECMAScript. Сегодня у Temporal есть около 4,5 тысячи тестов, а это много по сравнению с другими встроенными фичами, в том числе и с Date.

На пленарном заседании, проведённом в июне 2024 года, команда интернационализации Google и Boa решили совместно работать над реализацией Temporal, а также разработать библиотеку для Rust, совместимую с обоими движками. Эта библиотека называется temporal_rs. За 2024 и 2025 годы она благодаря работе Кевина Нэсса, Маниша Горегаокара, Хосе Эспины и студентов Бергенского университета сильно продвинула вперёд реализацию Temporal. Сегодня temporal_rs проходит 100% тестов и наряду с V8 и Boa уже обслуживает другие движки!
temporal_rs — довольно необычный проект. Очень редко бывает так, чтобы несколько движков совместно работало над общей библиотекой, реализующей предложение TC39. Всё не только получилось, но и завершилось огромным успехом. Благодаря temporal_rs:
Снизился барьер входа: студентам и другим разработчикам не нужно было разбираться в кодовой базе V8 или Boa для внесения своего вклада в создание библиотеки.
Улучшилось долгосрочное обслуживание: у temporal_rs есть команда мейнтейнеров, готовых продолжать работу над библиотекой даже после того, как Temporal достигнет Этапа 4. При этом у разработчиков будет стабильное место для публикации issue, отчётов о багах и даже самостоятельного внесения улучшений.
Проверять предстоит более качественный код: так как temporal_rs оформлена в виде библиотеки, выполнять её ревью будет проще, ведь не понадобится контекст всего движка. Кроме того, библиотека использует современные фичи Rust, например, встроенный линтер (Clippy), форматирование (Rustfmt) и тесты CI в движках.


Temporal уже достиг Этапа 4 в процессе подготовки TC39, то есть он станет частью следующей ежегодной спецификации ECMAScript (ES2026). Но вам не нужно ждать этого момента, им можно пользоваться уже сегодня!
Temporal уже имеет поддержку:
Firefox v139 (с мая 2025 года).
Chrome v144 (с января 2026 года).
Edge v144 (с января 2026 года).
TypeScript 6.0 Beta (с февраля 2026 года).
Safari (частичная поддержка в Technology Preview).
Node.js v26 (будет подтверждена позже).
Работа над Temporal ещё далеко не завершена, например, нужно разобраться, как он интегрируется с остальной экосистемой веба. Веб-API уже многие годы работали с объектом Date, и те же самые API должны быть совместимыми с объектами Temporal. Вот несколько примеров:
Разработчики захотят использовать Temporal с виджетами выбора даты. На данный момент это невозможно (вероятно, это можно пропатчить через полифил, но стандартного решения пока не существует). В процессе улучшения эргономики использования Temporal нам понадобится добавлять поддержку в тех областях, где сегодня применяется Date. Один из примеров этого — input type, связанные с датами и временем:
<input type="date" /> <!-- element.valueAsPlainDate --> <input type="time" /> <!-- element.valueAsPlainTime --> <input type="week" /> <!-- element.valueAsPlainDate --> <input type="month" /> <!-- element.valueAsPlainYearMonth -->
Так как Instant в Temporal поддерживают время до наносекунд, их можно применять там, где используется DOMHighResTimeStamp. В примере ниже можно использовать Instant для указания момента истечения срока действия куки, для чего ранее применялся DOMHighResTimeStamp.
cookieStore.set({ name: "foo", value: "bar", expires: Temporal.Now.instant().add({ hours: 24 }).epochMilliseconds, });
И таких примеров много. Одно понятно наверняка — сообщество JavaScript продолжит упорно работать над тем, чтобы Temporal можно было использовать не только на веб-платформе, но и в любых других библиотеках, которые сегодня применяют Date.
Temporal — результат почти десятка лет совместной работы компаний, разработчиков движков и отдельных контрибьюторов. Он отражает:
Годы наработки консенсуса внутри TC39, основанного непосредственно на предшествующем этапе развития веба.
Работу по реализации в нескольких движках JavaScript.
Сотрудничество между Microsoft, Google, Mozilla, Bloomberg, Igalia, Boa и множеством независимых контрибьюторов.
Редкий пример реализации общей инфраструктуры в виде библиотеки.
Мы гордимся тем, что в течение долгих лет спонсировали и поддерживали работу Igalia над Temporal. Эти вложения, наряду с открытым сотрудничеством, помогли превратить предложение из идеи в спецификацию.
Успех temporal_rs демонстрирует нечто важное: новые фичи языка необязательно требуют дублирующейся работы в разных движках. Общая высококачественная опенсорсная инфраструктура позволяет снижать затраты, повышать согласованность и ускорять инновации в экосистеме веба.
Temporal — это не просто улучшенный API, а доказательство того, что сообщество JavaScript способно вместе решать многолетние проблемы.
Спустя почти тридцать лет у JavaScript наконец появился современный API для работы с датами и временем.
И на этот раз мы сделали всё правильно.