2 сентября 2008 года Google выпустил Chrome. Вместе с ним - 38-страничный комикс от Scott McCloud, объясняющий архитектурные решения. Google рассказывал о браузере в жанре комикса. Это сразу давало понять: здесь всё иначе.
V8 - JavaScript-движок Chrome - писался с нуля Lars Bak и командой из Орхуса, Дания. Тем же людьми, что строили виртуальные машины для Smalltalk, Self и Java в 90-х. Они знали JIT-компиляцию лучше кого-либо.
Firefox 3 на SunSpider: 3800мс. Chrome beta: 380мс. Один и тот же JavaScript, одна и та же машина, в десять раз быстрее.
Мы смотрели на эту цифру и не понимали, как это возможно.
Что делает V8 иначе
Традиционные JS-движки (SpiderMonkey до 2008, JavaScriptCore) интерпретировали байткод. V8 компилировал напрямую в машинный код при первом выполнении:
// Этот код в 2008 году выполнялся принципиально по-разному
// в Firefox 3 (интерпретатор) и Chrome (JIT)
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Firefox 3: ~2400мс для fibonacci(40)
// Chrome beta: ~240мс для fibonacci(40)
// Одинаковый код. 10x разница.
// Ключевая оптимизация V8: Hidden Classes.
// Вместо хэш-таблиц для свойств объекта - структура, как в C++:
// При создании объекта V8 создаёт "скрытый класс":
var point = {}; // Hidden Class C0: {}
point.x = 10; // Hidden Class C1: {x}
point.y = 20; // Hidden Class C2: {x, y}
// Если все объекты одного "типа" создаются одинаково -
// они разделяют Hidden Class, доступ к свойствам = offset в памяти.
// Это C++-скорость для JS-объектов.
// Антипаттерн, ломающий это:
function Point(x, y) {
this.x = x;
this.y = y;
}
var p1 = new Point(1, 2); // C2: {x, y}
// Не делайте так:
var p2 = new Point(3, 4);
p2.z = 5; // Отдельный Hidden Class - деоптимизация
Benchmark-война: SunSpider 2008
// SunSpider 0.9 - стандарт измерения JS-производительности в 2008
// Запускался на каждом браузере, результаты публиковались в блогах
// Один из тестов - битовые операции на строках
// (упрощённая версия того что делал реальный тест)
function bitops_bits_in_byte(b) {
b = b - ((b >> 1) & 0x55555555);
b = (b & 0x33333333) + ((b >> 2) & 0x33333333);
return ((b + (b >> 4) & 0xF0F0F0F) * 0x1010101) >> 24;
}
var result = 0;
for (var i = 0; i < 500000; i++) {
result += bitops_bits_in_byte(i);
}
// Chrome (V8): ~15мс
// Firefox 3 (SpiderMonkey): ~180мс
// IE 7 (JScript): ~1200мс
// Safari 3 (JavaScriptCore): ~90мс
// После этих чисел Mozilla запустила Tamarin (затем TraceMonkey)
// Mozilla анонсировала 40x ускорение JavaScript в Firefox 3.1
// Safari начал работу над Squirrelfish Extreme (Nitro)
// Началась гонка JS-движков
Process-per-tab: изоляция как архитектурное решение
// Из комикса Chrome 2008:
// "Каждая вкладка = отдельный процесс ОС"
// Последствие для разработчиков:
// 1. Один crashed tab не убивает браузер
// 2. Каждая вкладка = отдельный JS heap
// 3. Вкладки изолированы по памяти (sandbox)
// Для веб-разработки в 2008 это меняло понимание:
// Раньше мы думали о "состоянии браузера"
// Теперь - о "состоянии вкладки"
// window.name hack - передача данных между страницами (до HTML5):
// Страница A:
window.name = JSON.stringify({ userId: 123, token: 'abc' });
location.href = 'page-b.html';
// Страница B:
try {
var data = JSON.parse(window.name);
console.log(data.userId); // 123
} catch(e) {}
// Chrome это не сломал, но process isolation делала очевидным:
// такие хаки - костыли. Нужен нормальный storage API.
Что Chrome запустил: реакция индустрии
Firefox 3.5 (июнь 2009): TraceMonkey - trace-based JIT компилятор. 20-40x ускорение числовых операций по сравнению с Firefox 3.
Safari 4 (февраль 2009): Squirrelfish Extreme (Nitro) - также JIT. Apple заявляла 4x улучшение против Safari 3.
IE 8 (март 2009): Не имел JIT. Microsoft осознавала отставание - IE9 (2011) получит Chakra с JIT.
// SunSpider результаты к концу 2009 - всё изменилось за год:
// Firefox 3.0: 3791мс → Firefox 3.5: 1036мс (-73%)
// Safari 3.1: 1688мс → Safari 4: 396мс (-77%)
// Chrome 1.0: 1071мс → Chrome 4: 370мс (-65%)
// IE 7: 5708мс → IE 8: 4691мс (-18%) ← проигрывает гонку
// Практическое следствие:
// JavaScript в 2009 году стал достаточно быстрым
// для сложных клиентских приложений.
// Gmail, Google Docs, Google Maps могли работать иначе.
// Начался SPA-era.
DevTools: рождение современной отладки
Chrome запустился с DevTools - инструментами разработчика, встроенными в браузер:
// Chrome DevTools 2008 - то, чего не было в Firebug (или было хуже):
// Профайлер JavaScript:
// Вкладка Profiles → CPU Profile → запись 5 секунд →
// граф вызовов с временем на каждую функцию.
// Раньше: console.time() / console.timeEnd() вручную.
// Пример оптимизации, ставшей возможной с профайлером:
// До профайлера - писали "оптимизированный" код вслепую:
for (var i = 0; i < arr.length; i++) { // arr.length каждую итерацию
process(arr[i]);
}
// С профайлером нашли реальный bottleneck - не цикл, а process():
function process(item) {
// Вот тут 40% времени - создание DOM-элементов в цикле
var el = document.createElement('div');
el.innerHTML = item.html;
document.body.appendChild(el); // reflow на каждой итерации!
}
// Исправление: DocumentFragment
var frag = document.createDocumentFragment();
for (var i = 0; i < arr.length; i++) {
var el = document.createElement('div');
el.innerHTML = arr[i].html;
frag.appendChild(el); // нет reflow
}
document.body.appendChild(frag); // один reflow
Что изменил Chrome к концу 2008
Когда Chrome вышел, JavaScript был языком для DOM-манипуляций и Ajax-запросов. Сложные вычисления делали на сервере - JavaScript был слишком медленным.
Chrome доказал: проблема была не в языке, а в реализации. V8 показал, что JS может быть быстрым. Это изменило что разработчики считали возможным строить в браузере.
За следующие два года появились: Node.js (V8 на сервере), CoffeeScript, первые MVC-фреймворки (Backbone.js), WebSockets. Всё это стало возможным потому что 2 сентября 2008 года один браузер поставил под сомнение предположение, с которым все жили восемь лет.