Узнайте, как реализовать UUID v7 в браузере: структура, код на JavaScript, ловушки и когда использовать v4, v7 или ULID. Попробуйте прямо сейчас!
Случайный UUID v4 — плохой первичный ключ: его значения разбросаны по всему индексу, что вызывает частые разделения страниц B-дерева. UUID v7 помещает метку времени в старшие биты (стандартизирован в RFC 9562), благодаря чему порядок генерации совпадает с порядком сортировки, и локальность восстанавливается. В этой статье я покажу, как вручную реализовать UUID v7 в браузере, разберу структуру, подводные камни и дам практические рекомендации.
128 бит UUID v7 делятся так:
| 48 бит unix_ms | 4 бит ver(=7) | 12 бит rand_a | 2 бит variant | 62 бит rand_b |Первые 48 бит — это Unix-время в миллисекундах, порядок big-endian. Затем идут биты версии (7) и варианта, а всё остальное — случайные данные. Случайная часть должна быть криптостойкой (crypto.getRandomValues), а не Math.random().
Задача — упаковать метку времени в первые шесть байт (старший байт первым), затем перезаписать полубайты версии и варианта.
function rnd(n) {
const a = new Uint8Array(n);
crypto.getRandomValues(a);
return a;
}
function hex(b) {
let s = '';
for (const x of b) s += ('0' + x.toString(16)).slice(-2);
return s;
}
function uuidV7() {
const ts = Date.now(); // 48-битная метка времени
const b = rnd(16);
// 48-битная метка времени big-endian (первые шесть байт)
b[0] = (ts / 0x10000000000) & 0xff; // ts >> 40
b[1] = (ts / 0x100000000) & 0xff; // ts >> 32
b[2] = (ts / 0x1000000) & 0xff; // ts >> 24
b[3] = (ts / 0x10000) & 0xff; // ts >> 16
b[4] = (ts / 0x100) & 0xff; // ts >> 8
b[5] = ts & 0xff;
b[6] = (b[6] & 0x0f) | 0x70; // версия 7
b[8] = (b[8] & 0x3f) | 0x80; // вариант (10xx)
const h = hex(b);
return `${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20)}`;
}Важный нюанс: сдвигать биты нужно делением, а не оператором >>. ts >> 40 не сработает, потому что побитовые операторы в JavaScript работают с 32-битными числами, и 48-битное значение будет испорчено. Деление с последующим & 0xff безопасно.
Для v4, если платформа предоставляет стандартный API, не изобретайте велосипед — это быстрее и безопаснее.
function uuidV4() {
if (crypto.randomUUID) return crypto.randomUUID(); // один вызов там, где поддерживается
const b = rnd(16);
b[6] = (b[6] & 0x0f) | 0x40;
b[8] = (b[8] & 0x3f) | 0x80;
const h = hex(b);
return `${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20)}`;
}Если два ID сгенерированы в одну миллисекунду, их первые 48 бит совпадают, а всё остальное случайно — относительный порядок произволен. Если вы предполагаете, что порядок генерации строго возрастает, то при всплеске ID это предположение нарушится.
Когда нужна строгая монотонность, используйте 12-битное поле rand_a как счётчик: увеличивайте его в пределах одной миллисекунды и сбрасывайте при смене миллисекунды.
let lastMs = 0, seq = 0;
function uuidV7Monotonic() {
const ts = Date.now();
if (ts === lastMs) {
seq = (seq + 1) & 0x0fff; // та же мс: +1 (12 бит)
} else {
lastMs = ts;
seq = rnd(2)[0] & 0x0fff; // новая мс: случайное начало
}
// ...сохраняем seq в младшие 12 бит b[6..7], перезаписывая версию = 7...
}Если для вашего случая достаточно «примерно упорядоченных по времени», обычная случайность подойдёт. Главное решение — нужно ли вам строгая монотонность, а не то, какая реализация выглядит умнее.
Я проверил следующее как в Node, так и в браузере, вместо того чтобы полагаться на спецификацию:
u[14] === '7', и u[19] — один из 8, 9, a, b).Воспроизвести сбой самостоятельно — вот что делает границу ловушки видимой. «Спецификация говорит, что должно» — это не то же самое, что увидеть это своими глазами.
| Сортируемость по времени | Локальность | Стандарт | В одной строке | |
|---|---|---|---|---|
| UUID v4 | Нет | Низкая | RFC 9562 | Полностью случайный. Хорошо распределяется, но плох для индексов |
| UUID v7 | Да (миллисекунды) | Высокая | RFC 9562 | Хороший первичный ключ. Подходит для существующего столбца UUID |
| ULID | Да | Высокая | Де-факто | 26-символьный Base32. Стоит рассмотреть, если не нужна совместимость с UUID |
Если вы используете UUID как первичные ключи, v7 стоит рассмотреть в первую очередь. На практике большое преимущество в том, что он подходит для существующего столбца uuid/UUID без миграции — нужно сменить только генератор.
Я опубликовал генератор как браузерный инструмент: он поддерживает v4 и v7, с опциями количества, дефисов и верхнего регистра. Генерация происходит на странице, ничего не отправляется на сервер: https://hashitosystem.com/tools/uuidgen/
UUID v7 — правильный выбор, если нужны «примерно упорядоченные по времени ключи с хорошей локальностью». Реализация — просто разбиение метки времени на байты делением и перезапись полубайтов версии/варианта. Единственное предостережение: порядок в пределах одной миллисекунды не гарантирован, поэтому используйте счётчик, если нужна строгая монотонность. Учтите этот момент — и v7 можно использовать смело.
Что делать прямо сейчас: откройте консоль браузера и скопируйте функцию uuidV7 из статьи. Сгенерируйте 10 ID и проверьте, что они упорядочены по времени. Затем попробуйте сгенерировать 1000 ID в одном цикле и убедитесь, что порядок нарушается. Если ваш проект требует строгой монотонности, реализуйте версию со счётчиком. Это займёт 5 минут, но сэкономит часы отладки.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →