Веб-сайт Quip обрабатывается на стороне клиента с помощью React. Обычно мы загружаем данные через HTTPS, используя нашу архитектуру синхронизатора, но, чтобы избежать лишних циклов обработки первого отображаемого экрана, мы встраиваем их в исходный HTML-ответ. После того, как сервер загрузил данные, которые потребуются клиенту, он выводит их с помощью простого тега <script> в форме:

<script>
  app.desktop.main([JSON data]);
</script>

Это очень распространенный шаблон, и многие клиентские приложения (возвращаясь к Gmail) используют этот подход. В случае Quip данные JSON могут варьироваться от небольших (для постоянной ссылки экрана Обновления в небольшой учетной записи) до довольно больших (сотни килобайт для большого экрана документа в учетной записи с большим количеством контактов).

В какой-то момент прошлой осенью Белинда обратилась ко мне по поводу устранения проблемы с производительностью при загрузке больших таблиц. Выполняя профилирование, мы были удивлены как использованием памяти, так и большим количеством времени, затраченного на создание JavaScript AST. Поразмыслив немного, я понял, что в этом есть смысл: браузер анализировал тег <script> как произвольный JavaScript. У него не было возможности узнать, что поддерево [JSON data] было полностью JSON и поэтому могло быть проанализировано непосредственно в простую структуру данных с помощью гораздо более эффективного парсера.

Это напомнило мне о разнице между использованием eval и JSON.parse для анализа данных JSON в сетевых ответах. Хотя тестировать сложно, это действительно тот случай, когда JSON.parse работает немного быстрее, потому что он должен делать намного меньше работы. Мне было интересно, могу ли я применить тот же подход к данным, которые мы встраиваем в наш HTML. Я придумал такое разделение между кодом и данными (здесь для краткости вставлено):

<script type="application/json">
  [JSON data]
</script>
<script>
  const jsonNode = document.querySelector(
      "script[type='application/json']");
  const jsonText = jsonNode.textContent;
  const jsonData = JSON.parse(jsonText);
  app.desktop.main(jsonData);
</script>

Предоставляя данным JSON нестандартный type в теге <script>, браузер не будет пытаться анализировать их как JavaScript (это будет рассматриваться как инертный блок данных »). Затем мы можем получить необработанный текст и проанализировать его более эффективно с помощью JSON.parse.

Для моей учетной записи Quip, загружающей большой документ (ответ HTML составляет 550 КБ), это требует сквозного (переход к -render) с 1830 мс до 1758 мс на основе некоторых простых тестов. Хотя это и небольшой выигрыш, он заметен и почти не требует дополнительных сложностей.

Чтобы измерить влияние этого подхода в более контролируемых обстоятельствах, я создал простой тестовый стенд (источник ). Он проверяет время сквозной загрузки с использованием обоих подходов с использованием данных разного размера² на разных платформах³:

Чем больше размер JSON, тем больше польза. Кроме того, превращение JSON в инертный скрипт дает больше преимуществ мобильным платформам с ограниченным ЦП.

Одно интересное открытие, которое я сделал во время работы над этим тестом, заключалось в том, что у WebKit есть код, который пытается обнаружить этот шаблон. Моя первоначальная привязка давала идентичные результаты как с JS, так и с инертным подходом на моем iPhone. Только когда я добавил вызов функции без операции в фрагмент JS, я получил ожидаемую разницу в производительности. Я предполагаю, что сработал детектор JSONP, поскольку <script> состоял исключительно из вызова функции с параметром JSON. На самом деле встроенные данные JSON Quip находятся в более сложном <script>, поэтому создание эталонного теста, имитирующего эту сложность, казалось целесообразным.

Все это было в контексте веб-приложения Quip, но наше гибридное настольное приложение может также извлекают выгоду из этого разделения данных и кода. На Mac мы используем WebView, которому даны данные с использованием stringByEvaluatingJavaScriptFromString. Параметр JavaScript - это форма вызова функции platform.native.handleReponse([JSON data]), которая очень похожа на фрагмент HTML, с которого мы начали. Если бы вместо этого мы могли загружать данные JSON более напрямую, оцениваемый JavaScript-код состоял бы только из кода. К счастью, мы можем получить доступ к контексту JavaScript WebView с помощью общедоступных API, и там мы можем использовать JSValueMakeFromJSONString для эффективной загрузки данных JSON.

Собирая все вместе, это приводит к:

// Some way to get the JSON as a JSStringRef
JSStringRef pbLiteArgJsString = ...;
// Parse the JSON string as JSON using the JavaScriptCore API 
// that bypasses traditional JS parsing.
JSContextRef jsContext = webView.mainFrame.globalContext;
JSValueRef pbLiteArgJsValue = JSValueMakeFromJSONString(
    jsContext, pbLiteArgJsString);
// Release the string version of the JSON data as soon as 
// possible, to minimize the peak memory use.
JSStringRelease(pbLiteArgJsString);
// Make the JSON data available as a generatedPbLiteArg_N 
// global variable
static int pbLiteArgNameCounter = 0;
NSString *pbLiteArgName = [NSString stringWithFormat:
    @"generatedPbLiteArg_%d",pbLiteArgNameCounter++];
JSStringRef pbLiteArgNameJsString = JSStringCreateWithCFString(
    (__bridge CFStringRef) pbLiteArgName);
JSObjectSetProperty(
    jsContext, 
    JSContextGetGlobalObject(jsContext), 
    pbLiteArgNameJsString, 
    pbLiteArgJsValue, 
    kJSPropertyAttributeNone, 
    NULL);
JSStringRelease(pbLiteArgNameJsString);
// Generate the function call that uses the generatedPbLiteArg_N 
// global variable as the parameter name. It then deletes 
// the reference as soon as the code runs, to avoid leaking memory.
NSString *js = [NSString stringWithFormat:
    @"platform.native.handleReponse(%@); delete %@;",
    pbLiteArgName, pbLiteArgName];
[webView stringByEvaluatingJavaScriptFromString:js];

В моем тестировании с большой электронной таблицей (~ 20 000 ячеек) в нашем приложении для Mac время загрузки данных заняло от 1014 мс до 533 мс, а пиковое использование памяти - с 783 МБ до 603 МБ. В iOS мы по-прежнему используем UIWebView, и хотя официально документированного способа получения контекста JavaScript не существует, существуют обходные пути, и они также должны быть совместимы с этим методом.

Это разделение между данные и код напоминают мне гарвардскую архитектуру, поэтому позвольте никому не говорить вам, что компьютерная архитектура середины 1940-х годов не имеет никакого отношения к современной веб-разработке.

  1. Пока сервер выполняет эту загрузку, начальная часть ответа HTML (содержащая тег <link> для нашей таблицы стилей и <script src="..."> для нашего JavaScript) сбрасывается, так что браузер может загружать и анализировать их параллельно с извлечением и передачей данных. .
  2. Размеры данных следующие: большой - 1,7 МБ JSON, средний - 130 КБ, маленький - 10 КБ и крошечный - 781 байт. Данные JSON должны напоминать JsPbLite, поскольку именно это Quip использует в качестве системы сериализации. JSON является частью шаблона HTML-документа, который загружается в iframe (через URL-адрес большого двоичного объекта), который затем сообщает родителю, когда он проанализирует ввод.
  3. Mac Pro: Chrome 65.0.3311.3 на Mac Pro 2010 (12 ядер) под управлением macOS 10.12.6
    iPhone X: Safari на iPhone X под управлением iOS 11.2.1
    Pixel 2: Chrome 63.0.3239.111 на Pixel 2 работает под управлением Android 8.1.0 (спасибо Akshay; Neil предоставил цифры для Nexus 6P, и ускорение было аналогичным)