Веб-сайт 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-х годов не имеет никакого отношения к современной веб-разработке.
- Пока сервер выполняет эту загрузку, начальная часть ответа HTML (содержащая тег
<link>
для нашей таблицы стилей и<script src="...">
для нашего JavaScript) сбрасывается, так что браузер может загружать и анализировать их параллельно с извлечением и передачей данных. . - Размеры данных следующие: большой - 1,7 МБ JSON, средний - 130 КБ, маленький - 10 КБ и крошечный - 781 байт. Данные JSON должны напоминать JsPbLite, поскольку именно это Quip использует в качестве системы сериализации. JSON является частью шаблона HTML-документа, который загружается в iframe (через URL-адрес большого двоичного объекта), который затем сообщает родителю, когда он проанализирует ввод.
- 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, и ускорение было аналогичным)