· 8 years ago · Jul 17, 2018, 01:00 PM
1
2<!--
3Copyright 2017 Google Inc. All Rights Reserved.
4Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
5Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
6-->
7<!doctype html>
8<html lang="en">
9<head>
10 <meta charset="utf-8">
11 <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
12 <link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAADjklEQVR4AWI08P/HQEvAQrxSQKvlECfLFYXx75xCY2qmh89GbNvOMjb3v9jOOlxnFWxj206ebQ3b7q6q+z1rNagu8/zvPSZACAABpeUAA0miMgU7SA7JjCraFGwZwECOwvL75dWjsKgWBKtx0jvWo+vkBAFbACCkByMP6nMn48+AVgXB2fzSCwsv22/lMGlUhmJ0AE7BH8dyUUDbUEgN6RzJRSeaPxhdRYR0Inel+7Hd5lBiFpkMAxACc0394//9C4voFHDiAAGLpuOXebdfdHfctgwJKaZRLRKy6ItrSis6RBnVBgGtbHyKTEmJHQoEXoBCE5BCrDeA2ogMUIGDAKEBDEhUqwgMqBYDjW4DQzmuffVdqff42/ZQYYqVcMXGZsMPyCsH3lyJSetxvEaxAQXdjR1HjfwCdIS7lo2DZke26Qe+MXO12OWkGT0O6oE7vMGkMnkYw4aN1KQgMKExhXqswfiov4+a7MQ11XPnbr/5qpKlgACAAQj94Lu271bN9DUecQasIZlNzG72llRAAKJiAi+/BSHrSFjRvQhg3DEKEqJh08tsmLTx597+f6enr4cc2Zpk57pihfX24dW7RHcOLLUbJYhJSl0ErQCI9BVXH/XrO97QasuvQQSiECa0BrQCIIJp6X9T/r8QG6L71WYSqCoIIGo2BZDUBnS/D9EA9Nun1iYvbM0MFExIDQRoKFatc1Z6zrm5uWeObJotq0BGV9FuQBWq5a4Fw3PPz848rZHstZSuA5FWAFSMP2nOppOOGpl6qh9PCSg0IFyHKjSQyDNQHTru2t75NOEe0fsf246oAmFkI6vCdnWvbQFQFCKx8vCswV8TrDLiDLgH4Nr7RAtNsrC9d8sfk7b8ls4igdNy8CQKAISlsB0FjZfd3Lfp155tf8fKI4BxZZIj/oTdVEAIAcJFOCmzauHG71I7/rdreUAgAqpDP05fDARCAQQARwEIBQSVxq0FyaLvZZtevpHa8WHw8cft6cpxlq8eAJtIhnSbWDf951yx3y13OqUuu5qyGgkxCgGFh9cDihDGbTa6BqvT1lWmrav3bmt2ZMJ4mU6TGgIC4DBzcv/JqAau1WhzSt3x9Ixk/4Jk/8J4ZrrViFMA4W6A7+WK8xcVjvyrOmVD0FbAXokcT48r+xVqLKvuJYbmpNadnlp3mpufJHOe/GXktM+r09bT8kEdq9BRYAbGSgzP7ll82U71Mc+ZFooXgwAAAABJRU5ErkJggg==">
13 <title>Lighthouse Report</title>
14 <style>/**
15 * @license Copyright 2017 Google Inc. All Rights Reserved.
16 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
17 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
18 */
19
20.lh-vars {
21 --text-font-family: Roboto, Helvetica, Arial, sans-serif;
22 --monospace-font-family: 'Menlo', 'dejavu sans mono', 'Consolas', 'Lucida Console', monospace;
23 --body-background-color: #fff;
24 --body-font-size: 14px;
25 --body-line-height: 18px;
26 --subheader-font-size: 14px;
27 --subheader-line-height: 20px;
28 --subheader-color: hsl(206, 6%, 25%);
29 --header-bg-color: #f1f3f4;
30 --header-font-size: 20px;
31 --header-line-height: 24px;
32 --title-font-size: 24px;
33 --title-line-height: 28px;
34 --caption-font-size: 12px;
35 --caption-line-height: 16px;
36 --default-padding: 12px;
37 --section-padding: 16px;
38 --section-indent: 16px;
39 --audit-group-indent: 16px;
40 --audit-item-gap: 5px;
41 --audit-indent: 16px;
42 --text-indent: 8px;
43 --expandable-indent: 20px;
44 --secondary-text-color: #565656;
45 /*--accent-color: #3879d9;*/
46 --fail-color: hsl(1, 73%, 45%);
47 --average-color: hsl(31, 100%, 45%); /* md orange 800 */
48 --pass-color: hsl(139, 70%, 30%);
49 --informative-color: #0c50c7;
50 --medium-75-gray: #757575;
51 --medium-50-gray: hsl(210, 17%, 98%);
52 --medium-100-gray: hsl(200, 12%, 95%);
53 --warning-color: #ffab00; /* md amber a700 */
54 --report-border-color: #ccc;
55 --report-secondary-border-color: #ebebeb;
56 --metric-timeline-rule-color: #b3b3b3;
57 --display-value-gray: hsl(216, 5%, 39%);
58 --report-width: calc(60 * var(--body-font-size));
59 --report-content-width: calc(var(--report-width));
60 --report-header-height: 161px;
61 --report-header-color: #202124;
62 --navitem-font-size: var(--body-font-size);
63 --navitem-line-height: var(--body-line-height);
64 --navitem-hpadding: var(--body-font-size);
65 --navitem-vpadding: calc(var(--navitem-line-height) / 2);
66 --lh-score-highlight-bg: hsla(0, 0%, 90%, 0.2);
67 --lh-score-icon-background-size: 24px;
68 --lh-score-margin: 12px;
69 --lh-table-header-bg: #f8f9fa;
70 --lh-table-higlight-bg: hsla(0, 0%, 75%, 0.1);
71 --lh-sparkline-height: 5px;
72 --lh-sparkline-thin-height: 3px;
73 --lh-filmstrip-thumbnail-width: 60px;
74 --lh-score-icon-width: calc(var(--body-font-size) / 14 * 16);
75 --lh-category-score-width: calc(5 * var(--body-font-size));
76 --lh-audit-vpadding: 8px;
77 --lh-audit-index-width: 18px;
78 --lh-audit-hgap: 12px;
79 --lh-audit-group-vpadding: 8px;
80 --lh-section-vpadding: 12px;
81 --chevron-size: 12px;
82
83 /* Voodoo magic here to get narrow columns. 0 doesn't size the column like our friend 1px does */
84 --bytes-col-width: 1px;
85
86 --pass-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><title>check</title><path fill="hsl(139, 70%, 30%)" d="M24 4C12.95 4 4 12.95 4 24c0 11.04 8.95 20 20 20 11.04 0 20-8.96 20-20 0-11.05-8.96-20-20-20zm-4 30L10 24l2.83-2.83L20 28.34l15.17-15.17L38 16 20 34z"/></svg>');
87 --average-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><title>info</title><path fill="hsl(31, 100%, 45%)" d="M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm2 30h-4V22h4v12zm0-16h-4v-4h4v4z"/></svg>');
88 --fail-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><title>warn</title><path fill="hsl(1, 73%, 45%)" d="M2 42h44L24 4 2 42zm24-6h-4v-4h4v4zm0-8h-4v-8h4v8z"/></svg>');
89
90 --av-timer-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><path d="M0 0h48v48H0z" fill="none"/><path d="M22 34c0 1.1.9 2 2 2s2-.9 2-2-.9-2-2-2-2 .9-2 2zm0-28v8h4v-3.84c6.78.97 12 6.79 12 13.84 0 7.73-6.27 14-14 14s-14-6.27-14-14c0-3.36 1.18-6.43 3.15-8.85L24 26l2.83-2.83-13.6-13.6-.02.04C8.84 12.89 6 18.11 6 24c0 9.94 8.04 18 17.99 18S42 33.94 42 24 33.94 6 23.99 6H22zm14 18c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2zm-24 0c0 1.1.9 2 2 2s2-.9 2-2-.9-2-2-2-2 .9-2 2z" fill="hsl(216, 5%, 39%)"/></svg>');
91 --photo-filter-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><path fill="none" d="M0 0h48v48H0V0z"/><path d="M38.04 20v18H10V10h18V6H10.04c-2.2 0-4 1.8-4 4v28c0 2.2 1.8 4 4 4h28c2.2 0 4-1.8 4-4V20h-4zM34 20l1.88-4.12L40 14l-4.12-1.88L34 8l-1.88 4.12L28 14l4.12 1.88zm-7.5 1.5L24 16l-2.5 5.5L16 24l5.5 2.5L24 32l2.5-5.5L32 24z" fill="hsl(216, 5%, 39%)"/></svg>');
92 --visibility-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><path d="M0 0h48v48H0z" fill="none"/><path d="M24 9C14 9 5.46 15.22 2 24c3.46 8.78 12 15 22 15 10.01 0 18.54-6.22 22-15-3.46-8.78-11.99-15-22-15zm0 25c-5.52 0-10-4.48-10-10s4.48-10 10-10 10 4.48 10 10-4.48 10-10 10zm0-16c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6z" fill="hsl(216, 5%, 39%)"/></svg>');
93 --check-circle-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><path d="M0 0h48v48H0z" fill="none"/><path d="M24 4C12.95 4 4 12.95 4 24c0 11.04 8.95 20 20 20 11.04 0 20-8.96 20-20 0-11.05-8.96-20-20-20zm-4 30L10 24l2.83-2.83L20 28.34l15.17-15.17L38 16 20 34z" fill="hsl(216, 5%, 39%)"/></svg>');
94 --check-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><path d="M0 0h48v48H0z" fill="none"/><path d="M18 32.34L9.66 24l-2.83 2.83L18 38l24-24-2.83-2.83z"/></svg>');
95 --search-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><path d="M31 28h-1.59l-.55-.55C30.82 25.18 32 22.23 32 19c0-7.18-5.82-13-13-13S6 11.82 6 19s5.82 13 13 13c3.23 0 6.18-1.18 8.45-3.13l.55.55V31l10 9.98L40.98 38 31 28zm-12 0a9 9 0 1 1 .001-18.001A9 9 0 0 1 19 28z" fill="hsl(216, 5%, 39%)"/><path d="M0 0h48v48H0z" fill="none" /></svg>');
96 --remove-circle-icon-url: url('data:image/svg+xml;utf8,<svg height="24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" fill="hsl(216, 5%, 39%)"/></svg>');
97}
98
99.lh-vars.lh-devtools {
100 --text-font-family: '.SFNSDisplay-Regular', 'Helvetica Neue', 'Lucida Grande', sans-serif;
101 --monospace-font-family: 'Menlo', 'dejavu sans mono', 'Consolas', 'Lucida Console', monospace;
102 --body-font-size: 12px;
103 --body-line-height: 16px;
104 --subheader-font-size: 14px;
105 --subheader-line-height: 18px;
106 --header-font-size: 16px;
107 --header-line-height: 20px;
108 --title-font-size: 20px;
109 --title-line-height: 24px;
110 --caption-font-size: 11px;
111 --caption-line-height: 14px;
112 --default-padding: 12px;
113 --section-padding: 12px;
114 --section-indent: 8px;
115 --audit-group-indent: 16px;
116 --audit-indent: 16px;
117 --expandable-indent: 16px;
118
119 --lh-audit-vpadding: 4px;
120 --lh-audit-hgap: 12px;
121 --lh-audit-group-vpadding: 12px;
122 --lh-section-vpadding: 8px;
123}
124
125@keyframes fadeIn {
126 0% { opacity: 0;}
127 100% { opacity: 0.6;}
128}
129
130.lh-root * {
131 box-sizing: border-box;
132}
133
134.lh-root {
135 font-family: var(--text-font-family);
136 font-size: var(--body-font-size);
137 margin: 0;
138 line-height: var(--body-line-height);
139 background: var(--body-background-color);
140 scroll-behavior: smooth;
141}
142
143.lh-root :focus {
144 outline: -webkit-focus-ring-color auto 3px;
145}
146.lh-root summary:focus {
147 outline: 1px solid hsl(217, 89%, 61%);
148}
149
150
151.lh-root [hidden] {
152 display: none !important;
153}
154
155.lh-audit-group a,
156.lh-category-header__description a {
157 color: #0c50c7;
158}
159
160
161.lh-audit__description,
162.lh-load-opportunity__description,
163.lh-details {
164 --inner-audit-left-padding: calc(var(--text-indent) + var(--lh-audit-index-width) + 2 * var(--audit-item-gap));
165 --inner-audit-right-padding: calc(var(--text-indent) + 2px);
166 margin-left: var(--inner-audit-left-padding);
167 margin-right: var(--inner-audit-right-padding);
168}
169
170.lh-details {
171 font-size: var(--body-font-size);
172 margin-top: var(--default-padding);
173 margin-bottom: var(--default-padding);
174 /* whatever the .lh-details side margins are */
175 width: calc(100% - var(--inner-audit-left-padding) - var(--inner-audit-right-padding));
176}
177
178.lh-details.flex .lh-code {
179 max-width: 70%;
180}
181
182/* Report header */
183
184.report-icon {
185 opacity: 0.7;
186}
187.report-icon:hover {
188 opacity: 1;
189}
190.report-icon[disabled] {
191 opacity: 0.3;
192 pointer-events: none;
193}
194
195.report-icon--share {
196 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"/></svg>');
197}
198.report-icon--print {
199 background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 8H5c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3zm-3 11H8v-5h8v5zm3-7c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm-1-9H6v4h12V3z"/><path fill="none" d="M0 0h24v24H0z"/></svg>');
200}
201.report-icon--copy {
202 background-image: url('data:image/svg+xml;utf8,<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>');
203}
204.report-icon--open {
205 background-image: url('data:image/svg+xml;utf8,<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h4v-2H5V8h14v10h-4v2h4c1.1 0 2-.9 2-2V6c0-1.1-.89-2-2-2zm-7 6l-4 4h3v6h2v-6h3l-4-4z"/></svg>');
206}
207.report-icon--download {
208 background-image: url('data:image/svg+xml;utf8,<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>');
209}
210
211/* Node */
212.lh-node {
213 display: block;
214 font-family: var(--monospace-font-family);
215 word-break: break-word;
216 color: hsl(174, 100%, 27%);
217}
218.lh-node:hover {
219 background: hsl(0, 0%, 98%);
220 border-radius: 2px;
221}
222
223/* Score */
224
225.lh-audit__score-icon {
226 margin-left: var(--lh-score-margin);
227 width: var(--lh-score-icon-width);
228 height: var(--lh-score-icon-width);
229 background: none no-repeat center center / contain;
230}
231
232.lh-audit--pass .lh-audit__display-text {
233 color: var(--pass-color);
234}
235.lh-audit--pass .lh-audit__score-icon {
236 background-image: var(--pass-icon-url);
237}
238
239.lh-audit--average .lh-audit__display-text {
240 color: var(--average-color);
241}
242.lh-audit--average .lh-audit__score-icon {
243 background-image: var(--average-icon-url);
244}
245
246.lh-audit--fail .lh-audit__display-text {
247 color: var(--fail-color);
248}
249.lh-audit--fail .lh-audit__score-icon {
250 background-image: var(--fail-icon-url);
251}
252
253.lh-audit--informative .lh-audit__display-text {
254 color: var(--display-value-gray);
255}
256
257.lh-audit--informative .lh-audit__score-icon,
258.lh-audit--manual .lh-audit__score-icon {
259 visibility: hidden;
260}
261.lh-audit--error .lh-audit__score-icon {
262 display: none;
263}
264
265.lh-category-header__description,
266.lh-audit__description {
267 color: var(--secondary-text-color);
268}
269
270.lh-category-header__description {
271 font-size: var(--body-font-size);
272 margin: calc(var(--default-padding) / 2) 0 var(--default-padding);
273}
274
275
276.lh-audit__index,
277.lh-audit__title,
278.lh-audit__display-text,
279.lh-audit__score-icon,
280.lh-load-opportunity__sparkline,
281.lh-chevron-container {
282 margin: 0 var(--audit-item-gap);
283}
284.lh-audit__index {
285 margin-left: 0;
286}
287.lh-chevron-container {
288 margin-right: 0;
289}
290
291
292.lh-audit__header .lh-audit__index {
293 width: var(--lh-audit-index-width);
294}
295
296.lh-audit__title {
297 flex: 1;
298}
299
300/* Expandable Details (Audit Groups, Audits) */
301.lh-audit__header {
302 display: flex;
303 padding: var(--lh-audit-vpadding) var(--text-indent);
304 cursor: pointer;
305}
306
307.lh-audit--load-opportunity .lh-audit__header {
308 display: block;
309}
310
311.lh-audit__header:hover {
312 background-color: #F8F9FA;
313}
314
315.lh-audit-group__summary::-webkit-details-marker,
316.lh-expandable-details__summary::-webkit-details-marker {
317 display: none;
318}
319
320
321/* Perf Metric */
322
323.lh-metric-container {
324 display: flex;
325}
326
327.lh-metric-column {
328 flex: 1;
329}
330.lh-metric-column:first-of-type {
331 margin-right: 20px;
332}
333
334.lh-metric {
335 border-bottom: 1px solid var(--report-secondary-border-color);
336}
337
338.lh-metric__innerwrap {
339 display: flex;
340 justify-content: space-between;
341 padding: 11px var(--text-indent);
342}
343
344.lh-metric__details {
345 order: -1;
346}
347
348.lh-metric__title {
349 font-size: var(--body-font-size);
350 line-height: var(--body-line-height);
351 display: flex;
352}
353
354.lh-metric__name {
355 flex: 1;
356}
357
358.lh-metrics__disclaimer {
359 color: var(--medium-75-gray);
360 text-align: right;
361 margin: var(--lh-section-vpadding) 0;
362 padding: 0 var(--text-indent);
363}
364
365.lh-metric__description {
366 color: var(--secondary-text-color);
367}
368
369.lh-metric__value {
370 white-space: nowrap; /* No wrapping between metric value and the icon */
371}
372
373
374.lh-metric .lh-metric__value::after {
375 content: '';
376 width: var(--lh-score-icon-width);
377 height: var(--lh-score-icon-width);
378 background-size: contain;
379 display: inline-block;
380 vertical-align: text-bottom;
381 margin-left: calc(var(--body-font-size) / 2);
382}
383
384.lh-metric--pass .lh-metric__value {
385 color: var(--pass-color);
386}
387.lh-metric--pass .lh-metric__value::after {
388 background: var(--pass-icon-url) no-repeat 50% 50%;
389}
390
391
392.lh-metric--average .lh-metric__value {
393 color: var(--average-color);
394}
395.lh-metric--average .lh-metric__value::after {
396 background: var(--average-icon-url) no-repeat 50% 50%;
397}
398
399
400.lh-metric--fail .lh-metric__value {
401 color: var(--fail-color);
402}
403.lh-metric--fail .lh-metric__value::after {
404 background: var(--fail-icon-url) no-repeat 50% 50%;
405}
406
407.lh-metric--error .lh-metric__value,
408.lh-metric--error .lh-metric__description {
409 color: var(--fail-color);
410}
411
412/* Hide icon if there was an error */
413.lh-metric--error .lh-metric__value::after {
414 display: none;
415}
416
417/* Perf load opportunity */
418
419.lh-load-opportunity__cols {
420 display: flex;
421 align-items: flex-start;
422}
423
424.lh-load-opportunity__header .lh-load-opportunity__col {
425 background-color: var(--medium-50-gray);
426 color: var(--medium-75-gray);
427 text-align: center;
428 display: unset;
429 line-height: calc(2.3 * var(--body-font-size));
430}
431
432.lh-load-opportunity__col {
433 display: flex;
434 justify-content: space-between;
435}
436
437.lh-load-opportunity__col--one {
438 flex: 5;
439 margin-right: 2px;
440}
441.lh-load-opportunity__col--two {
442 flex: 4;
443}
444
445.lh-audit--load-opportunity .lh-audit__display-text {
446 text-align: right;
447 flex: 0 0 calc(3 * var(--body-font-size));
448}
449
450
451/* Sparkline */
452
453.lh-load-opportunity__sparkline {
454 flex: 1;
455 margin-top: calc((var(--body-line-height) - var(--lh-sparkline-height)) / 2);
456}
457
458.lh-sparkline {
459 height: var(--lh-sparkline-height);
460 width: 100%;
461}
462
463.lh-sparkline__bar {
464 height: 100%;
465 float: right;
466}
467
468.lh-audit--pass .lh-sparkline__bar {
469 background: var(--pass-color);
470}
471
472.lh-audit--average .lh-sparkline__bar {
473 background: var(--average-color);
474}
475
476.lh-audit--fail .lh-sparkline__bar {
477 background: var(--fail-color);
478}
479
480
481
482/* Filmstrip */
483
484.lh-filmstrip-container {
485 padding: 0 var(--expandable-indent);
486 margin: 0 auto;
487}
488
489
490.lh-filmstrip {
491 display: flex;
492 flex-direction: row;
493 justify-content: space-between;
494 padding-bottom: var(--default-padding);
495}
496
497.lh-filmstrip__frame {
498 text-align: right;
499 position: relative;
500}
501
502.lh-filmstrip__thumbnail {
503 border: 1px solid var(--report-secondary-border-color);
504 max-height: 100px;
505 max-width: 60px;
506}
507
508/* Audit */
509
510.lh-audit {
511 border-bottom: 1px solid var(--report-secondary-border-color);
512}
513
514.lh-audit:last-child {
515 border-bottom: none;
516}
517
518.lh-audit--error .lh-audit__display-text {
519 color: var(--fail-color);
520}
521
522/* Audit Group */
523
524.lh-audit-group {
525 padding: var(--lh-audit-group-vpadding) 0;
526 border-bottom: 1px solid var(--report-secondary-border-color);
527}
528
529.lh-audit-group:last-child {
530 border-bottom: none;
531}
532
533.lh-audit-group__header {
534 font-size: var(--subheader-font-size);
535 line-height: var(--subheader-line-height);
536 color: var(--subheader-color);
537 flex: 1;
538 font-weight: bold;
539}
540
541.lh-audit-group__header::before {
542 content: '';
543 width: calc(var(--subheader-font-size) / 14 * 24);
544 height: calc(var(--subheader-font-size) / 14 * 24);
545 margin-right: calc(var(--subheader-font-size) / 2);
546 background: var(--medium-100-gray) none no-repeat center / 16px;
547 display: inline-block;
548 border-radius: 50%;
549 vertical-align: middle;
550}
551
552/* A11y/Seo groups within Passed don't get an icon */
553.lh-audit-group--unadorned .lh-audit-group__header::before {
554 content: none;
555}
556
557
558.lh-audit-group--manual .lh-audit-group__header::before {
559 background-image: var(--search-icon-url);
560}
561.lh-passed-audits .lh-audit-group__header::before {
562 background-image: var(--check-icon-url);
563}
564.lh-audit-group--diagnostics .lh-audit-group__header::before {
565 background-image: var(--search-icon-url);
566}
567.lh-audit-group--opportunities .lh-audit-group__header::before {
568 background-image: var(--photo-filter-icon-url);
569}
570.lh-audit-group--metrics .lh-audit-group__header::before {
571 background-image: var(--av-timer-icon-url);
572}
573.lh-audit-group--not-applicable .lh-audit-group__header::before {
574 background-image: var(--remove-circle-icon-url);
575}
576
577/* Removing too much whitespace */
578.lh-audit-group--metrics {
579 margin-top: -28px;
580 border-bottom: none;
581}
582
583.lh-audit-group__summary {
584 display: flex;
585 justify-content: space-between;
586 padding-right: var(--text-indent);
587}
588
589.lh-audit-group__itemcount {
590 color: var(--display-value-gray);
591 margin: 0 10px;
592}
593
594.lh-audit-group__description {
595 font-size: var(--body-font-size);
596 color: var(--medium-75-gray);
597 margin: var(--lh-audit-group-vpadding) 0;
598}
599
600.lh-audit-group--unadorned .lh-audit-group__description {
601 margin-top: 0;
602}
603
604.lh-audit-explanation {
605 margin: var(--lh-audit-vpadding) 0 calc(var(--lh-audit-vpadding) / 2);
606 line-height: var(--caption-line-height);
607}
608
609.lh-audit--fail .lh-audit-explanation {
610 color: var(--fail-color);
611}
612
613/* Report */
614
615.lh-container {
616 display: flex;
617 max-width: var(--report-content-width);
618 word-wrap: break-word;
619 margin: 0 auto;
620}
621
622.lh-header-sticky {
623 position: sticky;
624 top: 0;
625 width: 100%;
626 z-index: 2;
627 will-change: transform;
628}
629.lh-header-plain {
630 margin-top: var(--section-padding);
631}
632
633.lh-header-container {
634 display: block;
635 margin: 0 auto;
636 max-width: var(--report-content-width);
637 position: relative;
638 word-wrap: break-word;
639}
640
641.lh-report {
642 background-color: #fff;
643}
644@media screen {
645 .lh-report {
646 width: var(--report-width);
647 }
648}
649
650.lh-exception {
651 font-size: large;
652}
653
654.lh-code {
655 white-space: normal;
656 margin-top: 0;
657 font-size: 85%;
658 word-break: break-word;
659}
660
661.lh-warnings {
662 --item-margin: calc(var(--body-line-height) / 6);
663 border: 1px solid var(--average-color);
664 border-radius: 4px;
665 margin: var(--lh-audit-vpadding) 0;
666 padding: calc(var(--lh-audit-vpadding) / 2) var(--lh-audit-vpadding);
667}
668
669.lh-warnings--toplevel {
670 --item-margin: calc(var(--header-line-height) / 4);
671 color: var(--secondary-text-color);
672 margin: var(--section-padding);
673 padding: var(--section-padding);
674}
675.lh-warnings ul {
676 padding-left: calc(var(--section-padding) * 2);
677 margin: 0;
678}
679.lh-warnings li {
680 margin: var(--item-margin) 0;
681}
682.lh-warnings li:last-of-type {
683 margin-bottom: 0;
684}
685
686.lh-scores-header {
687 display: flex;
688 justify-content: center;
689 overflow-x: hidden;
690 position: relative;
691 padding: var(--section-indent) calc(var(--section-indent) / 2) calc(var(--section-indent) * 2);
692}
693.lh-scores-header__solo {
694 padding: 0;
695 border: 0;
696}
697
698.lh-scorescale {
699 color: var(--medium-75-gray);
700 padding: 0 calc(var(--section-indent) * 1.5) 0;
701 text-align: right;
702 transform-origin: bottom right;
703 will-change: opacity; /* opacity is changed on scroll */
704}
705
706.lh-scorescale-range {
707 margin-left: 10px;
708}
709
710.lh-scorescale-range::before {
711 content: '';
712 width: var(--body-font-size);
713 height: calc(var(--body-font-size) * .60);
714 border-radius: 4px;
715 display: inline-block;
716 margin: 0 5px;
717}
718
719.lh-scorescale-range--pass::before {
720 background-color: var(--pass-color);
721}
722
723.lh-scorescale-range--average::before {
724 background-color: var(--average-color);
725}
726
727.lh-scorescale-range--fail::before {
728 background-color: var(--fail-color);
729}
730
731/* Hide category score gauages if it's a single category report */
732.lh-header--solo-category .lh-scores-wrapper {
733 display: none;
734}
735
736
737.lh-categories {
738 width: 100%;
739 overflow: hidden;
740}
741
742.lh-category {
743 --circle-size: calc(2.5 * var(--header-font-size));
744
745 padding: var(--section-padding);
746 border-top: 1px solid var(--report-border-color);
747}
748
749.lh-category:first-of-type {
750 padding-top: calc(2 * var(--section-padding));
751 border: none;
752}
753
754/* section hash link jump should preserve fixed header
755 https://css-tricks.com/hash-tag-links-padding/
756*/
757.lh-category > .lh-permalink {
758 margin-top: calc((var(--report-header-height) + var(--default-padding)) * -1);
759 padding-bottom: calc(var(--report-header-height) + var(--default-padding));
760 display: block;
761 visibility: hidden;
762}
763
764.lh-category-header {
765 font-size: var(--header-font-size);
766 min-height: var(--circle-size);
767 margin-bottom: var(--lh-section-vpadding);
768}
769
770.lh-category-header__title {
771 line-height: 24px;
772}
773
774.lh-category-header .lh-score__gauge .lh-gauge__label {
775 display: none;
776}
777
778
779.lh-category-header .lh-score__gauge {
780 float: right;
781}
782
783.lh-category .lh-score__gauge {
784 margin-left: var(--section-indent);
785}
786
787.lh-category-header .lh-audit__title {
788 font-size: var(--header-font-size);
789 line-height: var(--header-line-height);
790}
791
792.lh-passed-audits[open] summary.lh-passed-audits-summary {
793 margin-bottom: calc(var(--default-padding) * 2);
794}
795
796summary.lh-passed-audits-summary {
797 margin: calc(var(--default-padding) * 2) var(--default-padding);
798 margin-left: var(--default-padding);
799 margin-bottom: 0;
800 font-size: 15px;
801 display: flex;
802 align-items: center;
803}
804
805#lh-log {
806 position: fixed;
807 background-color: #323232;
808 color: #fff;
809 min-height: 48px;
810 min-width: 288px;
811 padding: 16px 24px;
812 box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);
813 border-radius: 2px;
814 margin: 12px;
815 font-size: 14px;
816 cursor: default;
817 transition: transform 0.3s, opacity 0.3s;
818 transform: translateY(100px);
819 opacity: 0;
820 -webkit-font-smoothing: antialiased;
821 bottom: 0;
822 left: 0;
823 z-index: 3;
824}
825
826#lh-log.show {
827 opacity: 1;
828 transform: translateY(0);
829}
830
831/* 964 fits the min-width of the filmstrip */
832@media screen and (max-width: 964px) {
833 .lh-report {
834 margin-left: 0;
835 width: 100%;
836 min-width: 400px;
837 }
838}
839
840@media print {
841 body {
842 -webkit-print-color-adjust: exact; /* print background colors */
843 }
844 .lh-container {
845 display: block;
846 }
847 .lh-report {
848 margin-left: 0;
849 padding-top: 0;
850 }
851 .lh-categories {
852 margin-top: 0;
853 }
854}
855
856.lh-table {
857 --image-preview-size: 24px;
858 border-collapse: collapse;
859}
860
861.lh-table thead {
862 background: var(--lh-table-header-bg);
863}
864.lh-table thead th {
865 color: var(--medium-75-gray);
866 font-weight: normal;
867}
868
869.lh-table tbody tr:nth-child(even) {
870 background-color: var(--lh-table-higlight-bg);
871}
872
873.lh-table th,
874.lh-table td {
875 padding: 8px 6px;
876}
877
878.lh-table-column--text,
879.lh-table-column--bytes,
880.lh-table-column--timespanMs,
881.lh-table-column--ms {
882 text-align: right;
883}
884
885.lh-table-column--thumbnail {
886 width: calc(var(--image-preview-size) * 2);
887}
888
889.lh-table-column--url {
890 text-align: left;
891 min-width: 250px;
892 white-space: nowrap;
893 max-width: 0;
894}
895
896/* Keep bytes columns narrow if they follow the URL column */
897.lh-table-column--url + th.lh-table-column--bytes,
898.lh-table-column--url + .lh-table-column--bytes + th.lh-table-column--bytes {
899 width: var(--bytes-col-width);
900}
901
902.lh-table-column--code {
903 max-width: var(--url-col-max-width);
904}
905
906.lh-text__url {
907 overflow: hidden;
908 text-overflow: ellipsis;
909}
910
911.lh-text__url:hover {
912 text-decoration: underline dotted #999;
913 text-decoration-skip-ink: auto;
914}
915
916.lh-text__url > .lh-text, .lh-text__url-host {
917 display: inline-block;
918}
919
920.lh-text__url-host {
921 margin-left: calc(var(--body-font-size) / 2);
922 opacity: 0.6;
923 font-size: 90%
924}
925
926.lh-thumbnail {
927 height: var(--image-preview-size);
928 width: var(--image-preview-size);
929 object-fit: contain;
930}
931
932/* Chevron
933 https://codepen.io/paulirish/pen/LmzEmK
934 */
935.lh-chevron {
936 --chevron-angle: 42deg;
937 width: var(--chevron-size);
938 height: var(--chevron-size);
939 margin-top: calc((var(--body-line-height) - 12px) / 2);
940}
941
942.lh-chevron__lines {
943 transition: transform 0.4s;
944 transform: translateY(var(--body-line-height));
945}
946.lh-chevron__line {
947 stroke: var(--display-value-gray);
948 stroke-width: var(--chevron-size);
949 stroke-linecap: square;
950 transform-origin: 50%;
951 transform: rotate(var(--chevron-angle));
952 transition: transform 300ms, stroke 300ms;
953}
954
955.lh-audit-group > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-right,
956.lh-audit-group[open] > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-left,
957.lh-audit > .lh-expandable-details .lh-chevron__line-right,
958.lh-audit > .lh-expandable-details[open] .lh-chevron__line-left {
959 transform: rotate(calc(var(--chevron-angle) * -1));
960}
961
962.lh-audit-group[open] > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-right,
963.lh-audit > .lh-expandable-details[open] .lh-chevron__line-right {
964 transform: rotate(var(--chevron-angle));
965}
966
967.lh-audit-group[open] > .lh-audit-group__summary > .lh-chevron .lh-chevron__lines,
968.lh-audit > .lh-expandable-details[open] .lh-chevron__lines {
969 transform: translateY(calc(var(--chevron-size) * -1));
970}
971
972
973
974/* Tooltip */
975.tooltip-boundary {
976 position: relative;
977}
978
979.tooltip {
980 position: absolute;
981 display: none; /* Don't retain these layers when not needed */
982 opacity: 0;
983 background: #ffffff;
984 min-width: 246px;
985 max-width: 275px;
986 padding: 15px;
987 border-radius: 5px;
988 text-align: initial;
989}
990/* shrink tooltips to not be cutoff on left edge of narrow viewports
991 45vw is chosen to be ~= width of the left column of metrics
992*/
993@media screen and (max-width: 535px) {
994 .tooltip {
995 min-width: 45vw;
996 padding: 3vw;
997 }
998}
999
1000.tooltip-boundary:hover {
1001 background-color: #F8F9FA;
1002}
1003
1004.tooltip-boundary:hover .tooltip {
1005 display: block;
1006 animation: fadeInTooltip 250ms;
1007 animation-fill-mode: forwards;
1008 animation-delay: 850ms;
1009 bottom: 100%;
1010 z-index: 1;
1011 will-change: opacity;
1012 right: 0;
1013}
1014
1015.tooltip::before {
1016 content: "";
1017 border: solid transparent;
1018 border-bottom-color: #fff;
1019 border-width: 10px;
1020 position: absolute;
1021 bottom: -20px;
1022 right: 6px;
1023 transform: rotate(180deg);
1024 pointer-events: none;
1025}
1026
1027@keyframes fadeInTooltip {
1028 0% { opacity: 0; }
1029 75% { opacity: 1; }
1030 100% { opacity: 1; filter: drop-shadow(1px 0px 1px #aaa) drop-shadow(0px 2px 4px hsla(206, 6%, 25%, 0.15)); }
1031}
1032</style>
1033</head>
1034<body class="lh-root lh-vars">
1035 <noscript>Lighthouse report requires JavaScript. Please enable.</noscript>
1036 <div hidden><!-- Lighthouse run warnings -->
1037<template id="tmpl-lh-warnings--toplevel">
1038 <div class="lh-warnings lh-warnings--toplevel">
1039 <strong>There were issues affecting this run of Lighthouse:</strong>
1040 <ul></ul>
1041 </div>
1042</template>
1043
1044<!-- Lighthouse score scale -->
1045<template id="tmpl-lh-scorescale">
1046 <div class="lh-scorescale">
1047 <span class="lh-scorescale-label">Score scale:</span>
1048 <span class="lh-scorescale-range lh-scorescale-range--fail">0-44</span>
1049 <span class="lh-scorescale-range lh-scorescale-range--average">45-74</span>
1050 <span class="lh-scorescale-range lh-scorescale-range--pass">75-100</span>
1051 </div>
1052</template>
1053
1054<!-- Toggle arrow chevron -->
1055<template id="tmpl-lh-chevron">
1056 <svg class="lh-chevron" title="See audits" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 100 100">
1057 <g class="lh-chevron__lines">
1058 <path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40" stroke="#707173"/>
1059 <path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50" stroke="#707173"/>
1060 </g>
1061 </svg>
1062</template>
1063
1064<!-- Lighthouse category header -->
1065<template id="tmpl-lh-category-header">
1066 <div class="lh-category-header">
1067 <div class="lh-score__gauge"></div>
1068 <span class="lh-category-header__title"></span>
1069 <div class="lh-category-header__description"></div>
1070 </div>
1071</template>
1072
1073<!-- Lighthouse audit -->
1074<template id="tmpl-lh-audit">
1075 <div class="lh-audit">
1076 <details class="lh-expandable-details">
1077 <summary class="lh-audit__header lh-expandable-details__summary">
1078 <span class="lh-audit__index"></span>
1079 <span class="lh-audit__title"></span>
1080 <span class="lh-audit__display-text"></span>
1081 <div class="lh-audit__score-icon"></div>
1082 <div class="lh-chevron-container"></div>
1083 </summary>
1084 <div class="lh-audit__description"></div>
1085 </details>
1086 </div>
1087</template>
1088
1089<!-- Lighthouse perf metric -->
1090<template id="tmpl-lh-metric">
1091 <div class="lh-metric">
1092 <div class="lh-metric__innerwrap tooltip-boundary">
1093 <span class="lh-metric__title"></span>
1094 <div class="lh-metric__value"></div>
1095 <div class="lh-metric__description tooltip"></div>
1096 </div>
1097 </div>
1098</template>
1099
1100<!-- Lighthouse perf opportunity -->
1101<template id="tmpl-lh-opportunity">
1102 <div class="lh-audit lh-audit--load-opportunity">
1103 <details class="lh-expandable-details">
1104 <summary class="lh-audit__header lh-expandable-details__summary">
1105 <div class="lh-load-opportunity__cols">
1106 <div class="lh-load-opportunity__col lh-load-opportunity__col--one">
1107 <span class="lh-audit__index"></span>
1108 <div class="lh-audit__title"></div>
1109 </div>
1110 <div class="lh-load-opportunity__col lh-load-opportunity__col--two">
1111 <div class="lh-load-opportunity__sparkline">
1112 <div class="lh-sparkline"><div class="lh-sparkline__bar"></div></div>
1113 </div>
1114 <div class="lh-audit__display-text"></div>
1115 <div class="lh-chevron-container" title="See resources"></div>
1116 </div>
1117 </div>
1118 </summary>
1119 <div class="lh-audit__description"></div>
1120 </details>
1121 </div>
1122</template>
1123
1124
1125<!-- Lighthouse perf opportunity header -->
1126<template id="tmpl-lh-opportunity-header">
1127 <div class="lh-load-opportunity__header lh-load-opportunity__cols">
1128 <div class="lh-load-opportunity__col lh-load-opportunity__col--one">
1129 Resource to optimize
1130 </div>
1131 <div class="lh-load-opportunity__col lh-load-opportunity__col--two">
1132 Estimated Savings
1133 </div>
1134 </div>
1135</template>
1136
1137
1138<!-- Lighthouse score container -->
1139<template id="tmpl-lh-scores-wrapper">
1140 <style>
1141 .lh-scores-wrapper__background,
1142 .lh-scores-wrapper__shadow {
1143 position: absolute;
1144 top: 0;
1145 left: 0;
1146 width: 100%;
1147 height: 100%;
1148 background: white;
1149 border-radius: 8px;
1150 }
1151 .lh-scores-wrapper__shadow {
1152 border-radius: 0;
1153 box-shadow: rgba(0, 0, 0, 0.2) 0px 3px 0px -2px
1154 }
1155 .lh-scores-container {
1156 padding-bottom: calc(var(--section-indent) / 2);
1157 position: relative;
1158 width: 100%;
1159 }
1160 </style>
1161 <div class="lh-scores-wrapper">
1162 <div class="lh-scores-container">
1163 <div class="lh-scores-wrapper__background"></div>
1164 <div class="lh-scores-wrapper__shadow"></div>
1165 </div>
1166 </div>
1167</template>
1168
1169
1170<!-- Lighthouse header -->
1171<template id="tmpl-lh-heading">
1172 <style>
1173 :root {
1174 --report-header-overlap-top: 30px;
1175 }
1176 .lh-header-bg {
1177 background-color: var(--header-bg-color);
1178 height: var(--report-header-height);
1179 left: 0;
1180 position: absolute;
1181 top: 0;
1182 width: 100%;
1183 will-change: transform;
1184 }
1185 .lh-lighthouse {
1186 position: absolute;
1187 top: var(--report-header-height);
1188 right: 50%;
1189 transform: translate3d(calc(var(--report-content-width) / 2), -100%, 0);
1190 opacity: 1;
1191 transform-origin: bottom right;
1192 will-change: transform, opacity;
1193 }
1194 .lh-header {
1195 width: 100%;
1196 height: var(--report-header-height);
1197 max-width: 100%; /* support text-overflow on url */
1198 position: relative;
1199 }
1200 .lh-metadata {
1201 flex: 1 1 0;
1202 padding: calc(var(--section-padding) / 2);
1203 padding-left: var(--section-indent);
1204 line-height: 20px;
1205 color: var(--report-header-color);
1206 z-index: 1;
1207 position: relative;
1208 }
1209 .lh-metadata__results {
1210 text-overflow: ellipsis;
1211 white-space: nowrap;
1212 }
1213 .lh-metadata__url {
1214 color: currentColor;
1215 }
1216 .lh-scores-wrapper {
1217 margin-top: -30px;
1218 transform: translateZ(1px);
1219 }
1220 .lh-scores-wrapper__shadow {
1221 opacity: 0;
1222 }
1223 .lh-scores-wrapper__background,
1224 .lh-scores-wrapper__shadow {
1225 box-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.1);
1226 border-radius: 8px;
1227 will-change: opacity, transform;
1228 transform-origin: top;
1229 }
1230
1231 .lh-product-info, .lh-toolbar__metadata {
1232 align-items: center;
1233 white-space: nowrap;
1234 color: #5F6369;
1235 display: flex;
1236 font-size: calc(var(--body-font-size) * 0.9);
1237 margin-left: var(--section-indent);
1238 opacity: 0;
1239 transform: translateY(-50%);
1240 will-change: opacity;
1241 }
1242 .lh-product-info__icon {
1243 height: 20px;
1244 margin-right: var(--default-padding);
1245 }
1246 .lh-toolbar {
1247 height: 50px;
1248 position: absolute;
1249 top: 25px;
1250 will-change: transform;
1251 display: flex;
1252 width: calc(100% - 70px); /* give room for export */
1253 }
1254 .lh-toolbar__metadata {
1255 overflow: hidden;
1256 text-overflow: ellipsis;
1257 width: 100%;
1258 }
1259 .lh-toolbar__url {
1260 color: currentColor;
1261 display: block;
1262 white-space: nowrap;
1263 margin-right: 2px;
1264 }
1265 .lh-export {
1266 position: absolute;
1267 right: var(--section-indent);
1268 transform: translateY(0);
1269 top: calc(var(--section-padding) / 2);
1270 will-change: transform;
1271 z-index: 2;
1272 }
1273 .lh-export__button {
1274 background-color: #fff;
1275 border: 1px solid #dadada;
1276 border-radius: 2px;
1277 cursor: pointer;
1278 outline: none;
1279 height: 32px;
1280 width: 48px;
1281 background-repeat: no-repeat;
1282 background-size: 20px;
1283 background-position: 50% 50%;
1284 will-change: transform;
1285 }
1286 .lh-export__button:focus,
1287 .lh-export__button.active {
1288 box-shadow: 1px 1px 3px #ccc;
1289 }
1290 .lh-export__button.active + .lh-export__dropdown {
1291 opacity: 1;
1292 clip: rect(0, 164px, 200px, 0);
1293 }
1294 .lh-export__dropdown {
1295 position: absolute;
1296 background-color: #fff;
1297 border: 1px solid var(--report-border-color);
1298 border-radius: 3px;
1299 padding: calc(var(--default-padding) / 2) 0;
1300 cursor: pointer;
1301 top: 36px;
1302 right: 0;
1303 box-shadow: 1px 1px 3px #ccc;
1304 min-width: 125px;
1305 clip: rect(0, 164px, 0, 0);
1306 opacity: 0;
1307 transition: all 200ms cubic-bezier(0,0,0.2,1);
1308 }
1309 .lh-export__dropdown a {
1310 display: block;
1311 color: currentColor;
1312 text-decoration: none;
1313 white-space: nowrap;
1314 padding: 0 12px;
1315 line-height: 2;
1316 }
1317 .lh-export__dropdown a:hover,
1318 .lh-export__dropdown a:focus {
1319 background-color: #efefef;
1320 outline: none;
1321 }
1322 .lh-export__dropdown .report-icon {
1323 cursor: pointer;
1324 background-repeat: no-repeat;
1325 background-position: 8px 50%;
1326 background-size: 18px;
1327 background-color: transparent;
1328 text-indent: 18px;
1329 }
1330 /* copy icon needs slight adjustments to look great */
1331 .lh-export__dropdown .report-icon--copy {
1332 background-size: 16px;
1333 background-position: 9px 50%;
1334 }
1335 /* save-as-gist option hidden in report */
1336 .lh-export__dropdown .lh-export--gist {
1337 display: none;
1338 }
1339 .lh-config {
1340 color: var(--secondary-text-color);
1341 }
1342 .lh-config__timestamp {
1343 font-size: var(--caption-font-size);
1344 display: block;
1345 }
1346 a.lh-config__emulation {
1347 color: inherit;
1348 text-decoration: none;
1349 }
1350 @media screen and (max-width: 964px) {
1351 .lh-export__dropdown {
1352 right: 0;
1353 left: initial;
1354 }
1355 }
1356 @media print {
1357 .lh-header {
1358 position: static;
1359 margin-left: 0;
1360 }
1361 }
1362/*
1363 TODO: Enable animating the clouds
1364 .lh-lighthouse__clouds {
1365 animation: panacross 30s linear infinite;
1366 animation-play-state: paused;
1367 }
1368 @keyframes panacross {
1369 0% { transform: translateX(0px); }
1370 77% { transform: translateX(-680px); }
1371 77.0001% { transform: translateX(195px); }
1372 100% { transform: translateX(0px); }
1373 } */
1374 </style>
1375 <div class="lh-header-bg"></div>
1376 <div class="lh-lighthouse">
1377 <svg width="217" height="148" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
1378 <defs>
1379 <mask id="a" x="-56" y="-54" width="284" height="202" maskUnits="userSpaceOnUse">
1380 <path d="M-56-54h284v202H-56z" fill="#fff"/>
1381 </mask>
1382 <linearGradient id="b" x1="-525.16" y1="560.08" x2="-524.23" y2="560.08" gradientTransform="matrix(91 0 0 -77 47797 43181)" gradientUnits="userSpaceOnUse">
1383 <stop offset="0" stop-color="#f1f3f4"/>
1384 <stop offset="1" stop-color="#fff"/>
1385 </linearGradient>
1386 </defs>
1387 <g mask="url(#a)">
1388 <path d="M95 47h24v2H95z" fill="#ec5548"/>
1389 <path d="M98 49h18v11H98z" fill="#fbc21b"/>
1390 <path d="M95 59h24v7H95z" fill="#ec5548"/>
1391 <path d="M97.63 66h19.74l2.63 47H95z" fill="#fff"/>
1392 <path d="M107 38a10 10 0 0 1 10 10v1H97v-1a10 10 0 0 1 10-10zM96.77 82.23l21-10.7.63 11.87-22.31 11.87zM95 110.8L119.1 98l.9 14H95z" fill="#ec5548"/>
1393 <path d="M0 148a177.58 177.58 0 0 1 217 0z" fill="#e8eaed"/>
1394 <path d="M103 49a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5z" fill="#fef0c8"/>
1395 <path d="M7 16l91 33.18v10L7 93z" fill="url(#b)"/>
1396 </g>
1397 <g mask="url(#a)" class="lh-lighthouse__clouds">
1398 <path d="M60 .19A9.77 9.77 0 0 1 61.93 0a9.44 9.44 0 0 1 9.24 7.83A7.24 7.24 0 0 1 79 14.45v.73A7.37 7.37 0 0 1 76.2 21h-31a7.44 7.44 0 0 1-1.2-4.09 7.31 7.31 0 0 1 7.26-7.36 6.84 6.84 0 0 1 1.28.1v-.11A9.51 9.51 0 0 1 60 .19m79.78 22.31h-17.9a4.37 4.37 0 0 1-.63-2.25 4.2 4.2 0 0 1 4.16-4.25 4.37 4.37 0 0 1 .72.06V16a5.35 5.35 0 0 1 10.64-1h.33a4.2 4.2 0 0 1 4.15 4.25 4.29 4.29 0 0 1-1.47 3.25zM163 62h-24.15a5.1 5.1 0 0 1-.85-2.81 5.65 5.65 0 0 1 6.59-5.19v-.08a7.07 7.07 0 0 1 7.24-6.92 7.15 7.15 0 0 1 7.17 5.64h.44a5.46 5.46 0 0 1 5.6 5.32A5.19 5.19 0 0 1 163 62z" fill="#fff"/>
1399 </g>
1400 </svg>
1401 </div>
1402
1403 <div class="lh-header-container">
1404 <div class="lh-header">
1405 <div class="lh-metadata">
1406 <div class="lh-metadata__results"><a href="" class="lh-metadata__url" target="_blank" rel="noopener"></a></div>
1407 <div class="lh-config">
1408 <span class="lh-config__timestamp"></span>
1409 <a href="#runtime-settings" class="lh-config__emulation"></a>
1410 </div>
1411 </div>
1412 </div>
1413
1414 <div class="lh-scores-wrapper-placeholder"></div>
1415
1416 <div class="lh-toolbar">
1417 <div class="lh-product-info">
1418 <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAADjklEQVR4AWI08P/HQEvAQrxSQKvlECfLFYXx75xCY2qmh89GbNvOMjb3v9jOOlxnFWxj206ebQ3b7q6q+z1rNagu8/zvPSZACAABpeUAA0miMgU7SA7JjCraFGwZwECOwvL75dWjsKgWBKtx0jvWo+vkBAFbACCkByMP6nMn48+AVgXB2fzSCwsv22/lMGlUhmJ0AE7BH8dyUUDbUEgN6RzJRSeaPxhdRYR0Inel+7Hd5lBiFpkMAxACc0394//9C4voFHDiAAGLpuOXebdfdHfctgwJKaZRLRKy6ItrSis6RBnVBgGtbHyKTEmJHQoEXoBCE5BCrDeA2ogMUIGDAKEBDEhUqwgMqBYDjW4DQzmuffVdqff42/ZQYYqVcMXGZsMPyCsH3lyJSetxvEaxAQXdjR1HjfwCdIS7lo2DZke26Qe+MXO12OWkGT0O6oE7vMGkMnkYw4aN1KQgMKExhXqswfiov4+a7MQ11XPnbr/5qpKlgACAAQj94Lu271bN9DUecQasIZlNzG72llRAAKJiAi+/BSHrSFjRvQhg3DEKEqJh08tsmLTx597+f6enr4cc2Zpk57pihfX24dW7RHcOLLUbJYhJSl0ErQCI9BVXH/XrO97QasuvQQSiECa0BrQCIIJp6X9T/r8QG6L71WYSqCoIIGo2BZDUBnS/D9EA9Nun1iYvbM0MFExIDQRoKFatc1Z6zrm5uWeObJotq0BGV9FuQBWq5a4Fw3PPz848rZHstZSuA5FWAFSMP2nOppOOGpl6qh9PCSg0IFyHKjSQyDNQHTru2t75NOEe0fsf246oAmFkI6vCdnWvbQFQFCKx8vCswV8TrDLiDLgH4Nr7RAtNsrC9d8sfk7b8ls4igdNy8CQKAISlsB0FjZfd3Lfp155tf8fKI4BxZZIj/oTdVEAIAcJFOCmzauHG71I7/rdreUAgAqpDP05fDARCAQQARwEIBQSVxq0FyaLvZZtevpHa8WHw8cft6cpxlq8eAJtIhnSbWDf951yx3y13OqUuu5qyGgkxCgGFh9cDihDGbTa6BqvT1lWmrav3bmt2ZMJ4mU6TGgIC4DBzcv/JqAau1WhzSt3x9Ixk/4Jk/8J4ZrrViFMA4W6A7+WK8xcVjvyrOmVD0FbAXokcT48r+xVqLKvuJYbmpNadnlp3mpufJHOe/GXktM+r09bT8kEdq9BRYAbGSgzP7ll82U71Mc+ZFooXgwAAAABJRU5ErkJggg==" alt="" class="lh-product-info__icon" />
1419 <span class="lh-product-info__name">Lighthouse</span>
1420 <span class="lh-product-info__version"></span>
1421 </div>
1422
1423 <div class="lh-toolbar__metadata">
1424 <a href="" class="lh-toolbar__url" target="_blank" rel="noopener"></a>
1425 <span class="lh-toggle-arrow" title="See report's runtime settings"></span>
1426 </div>
1427 </div>
1428
1429 <div class="lh-export">
1430 <button class="report-icon report-icon--share lh-export__button" title="Export report"></button>
1431 <div class="lh-export__dropdown">
1432 <a href="#" class="report-icon report-icon--print" data-action="print-summary">Print Summary</a>
1433 <a href="#" class="report-icon report-icon--print" data-action="print-expanded">Print Expanded</a>
1434 <a href="#" class="report-icon report-icon--copy" data-action="copy">Copy JSON</a>
1435 <a href="#" class="report-icon report-icon--download" data-action="save-html">Save as HTML</a>
1436 <a href="#" class="report-icon report-icon--download" data-action="save-json">Save as JSON</a>
1437 <a href="#" class="report-icon report-icon--open lh-export--viewer" data-action="open-viewer">Open in Viewer</a>
1438 <a href="#" class="report-icon report-icon--open lh-export--gist" data-action="save-gist">Save as Gist</a>
1439 </div>
1440 </div>
1441</template>
1442
1443
1444<!-- Lighthouse footer -->
1445<template id="tmpl-lh-footer">
1446 <style>
1447 .lh-footer {
1448 background-color: var(--header-bg-color);
1449 border-top: 1px solid var(--report-secondary-border-color);
1450 padding: var(--section-indent) calc(var(--default-padding) * 2);
1451 }
1452 .lh-footer .lh-generated {
1453 text-align: center;
1454 border-top: 1px solid var(--report-border-color);
1455 padding-top: var(--default-padding);
1456 }
1457 .lh-env {
1458 padding: var(--default-padding) 0;
1459 }
1460 .lh-env__items {
1461 padding-left: 16px;
1462 }
1463 span.lh-env__name {
1464 font-weight: bold;
1465 color: var(--secondary-text-color);
1466 }
1467 span.lh-env__description {
1468 font-family: var(--monospace-font-family);
1469 font-size: var(--caption-font-size);
1470 padding-left: 5px;
1471 }
1472 </style>
1473 <footer class="lh-footer">
1474 <div class="lh-env">
1475 <div class="lh-env__title">Runtime settings</div>
1476 <ul class="lh-env__items">
1477 <template id="tmpl-lh-env__items">
1478 <li class="lh-env__item">
1479 <span class="lh-env__name"></span>
1480 <span class="lh-env__description"></span>
1481 </li>
1482 </template>
1483 </ul>
1484 </div>
1485
1486 <div class="lh-generated">
1487 Generated by <b>Lighthouse</b> <span class="lh-footer__version"></span> |
1488 <a href="https://github.com/GoogleChrome/Lighthouse/issues" target="_blank" rel="noopener">File an issue</a>
1489 </div>
1490 </footer>
1491</template>
1492
1493<!-- Lighthouse score gauge -->
1494<template id="tmpl-lh-gauge">
1495 <style>
1496 .lh-gauge__wrapper {
1497 --circle-size: calc(3 * var(--header-font-size));
1498 --circle-size-half: calc(var(--circle-size) / 2);
1499 --circle-background: hsl(216, 12%, 92%);
1500 --circle-border-width: 9;
1501 --inset-size: calc(var(--circle-size) - 2 * var(--circle-border-width));
1502 --transition-length: 1s;
1503 }
1504
1505 .lh-gauge__wrapper--pass,
1506 .lh-gauge__wrapper--pass .lh-gauge {
1507 --circle-color: var(--pass-color);
1508 color: var(--circle-color);
1509 }
1510
1511 .lh-gauge__wrapper--average,
1512 .lh-gauge__wrapper--average .lh-gauge {
1513 --circle-color: var(--average-color);
1514 color: var(--circle-color);
1515 }
1516
1517 .lh-gauge__wrapper--fail,
1518 .lh-gauge__wrapper--fail .lh-gauge {
1519 --circle-color: var(--fail-color);
1520 color: var(--circle-color);
1521 }
1522
1523 .lh-gauge {
1524 max-width: 360px;
1525 max-height: 360px;
1526 stroke-linecap: round;
1527 width: var(--circle-size);
1528 height: var(--circle-size);
1529 }
1530
1531 .lh-gauge-base {
1532 fill: none;
1533 stroke: var(--circle-background);
1534 stroke-width: var(--circle-border-width);
1535 }
1536 .lh-gauge-arc {
1537 fill: none;
1538 stroke: var(--circle-color);
1539 stroke-width: var(--circle-border-width);
1540 animation: load-gauge var(--transition-length) ease forwards;
1541 animation-delay: 250ms;
1542 }
1543
1544 @keyframes load-gauge {
1545 from { stroke-dasharray: 0 329; }
1546 }
1547
1548 .lh-gauge__percentage {
1549 --spacer: calc((var(--circle-size) - var(--inset-size)) / 2);
1550 width: var(--inset-size);
1551 height: var(--inset-size);
1552 position: absolute;
1553 border-radius: inherit;
1554 font-size: var(--header-font-size);
1555 text-align: center;
1556 top: calc(var(--circle-size) / 3);
1557 }
1558
1559 .lh-gauge__wrapper {
1560 display: inline-flex;
1561 align-items: center;
1562 flex-direction: column;
1563 text-decoration: none;
1564 flex: 1;
1565 min-width: auto;
1566 position: relative;
1567
1568 /* Contain the layout style paint & layers during animation*/
1569 contain: content;
1570 will-change: opacity; /* Only using for layer promotion */
1571 }
1572
1573 .lh-gauge__label {
1574 font-size: var(--body-font-size);
1575 line-height: var(--body-line-height);
1576 margin-top: calc(0.5 * var(--body-line-height));
1577 text-align: center;
1578 color: black;
1579 }
1580
1581 </style>
1582 <a href="#" class="lh-gauge__wrapper">
1583 <svg viewBox="0 0 120 120" class="lh-gauge" fill="none" stroke-width="2">
1584 <circle class="lh-gauge-base" r="53" cx="60" cy="60"></circle>
1585 <circle class="lh-gauge-arc" transform="rotate(-90 60 60)" stroke-dasharray="0 329" stroke-dashoffset="0" r="53" cx="60" cy="60"></circle>
1586 </svg>
1587 <div class="lh-gauge__percentage"></div>
1588 <div class="lh-gauge__label"></div>
1589 </a>
1590</template>
1591
1592<!-- Lighthouse crtiical request chains component -->
1593<template id="tmpl-lh-crc">
1594 <div class="lh-crc-container">
1595 <style>
1596 .lh-crc .tree-marker {
1597 width: 12px;
1598 height: 26px;
1599 display: block;
1600 float: left;
1601 background-position: top left;
1602 }
1603 .lh-crc .horiz-down {
1604 background: url('data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><g fill="%23D8D8D8" fill-rule="evenodd"><path d="M16 12v2H-2v-2z"/><path d="M9 12v14H7V12z"/></g></svg>');
1605 }
1606 .lh-crc .right {
1607 background: url('data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><path d="M16 12v2H0v-2z" fill="%23D8D8D8" fill-rule="evenodd"/></svg>');
1608 }
1609 .lh-crc .up-right {
1610 background: url('data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><path d="M7 0h2v14H7zm2 12h7v2H9z" fill="%23D8D8D8" fill-rule="evenodd"/></svg>');
1611 }
1612 .lh-crc .vert-right {
1613 background: url('data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><path d="M7 0h2v27H7zm2 12h7v2H9z" fill="%23D8D8D8" fill-rule="evenodd"/></svg>');
1614 }
1615 .lh-crc .vert {
1616 background: url('data:image/svg+xml;utf8,<svg width="16" height="26" viewBox="0 0 16 26" xmlns="http://www.w3.org/2000/svg"><path d="M7 0h2v26H7z" fill="%23D8D8D8" fill-rule="evenodd"/></svg>');
1617 }
1618 .lh-crc .crc-tree {
1619 font-size: 14px;
1620 width: 100%;
1621 overflow-x: auto;
1622 }
1623 .lh-crc .crc-node {
1624 height: 26px;
1625 line-height: 26px;
1626 white-space: nowrap;
1627 }
1628 .lh-crc .crc-node__tree-value {
1629 margin-left: 10px;
1630 }
1631 .lh-crc .crc-node__chain-duration {
1632 font-weight: 700;
1633 }
1634 .lh-crc .crc-node__tree-hostname {
1635 color: #595959;
1636 }
1637 .lh-crc .crc-initial-nav {
1638 color: #595959;
1639 font-style: italic;
1640 }
1641 </style>
1642 <div>
1643 Longest chain: <b class="lh-crc__longest_duration"><!-- fill me: longestChain.duration --></b>
1644 over <b class="lh-crc__longest_length"><!-- fill me: longestChain.length --></b> requests, totalling
1645 <b class="lh-crc__longest_transfersize"><!-- fill me: longestChain.length --></b>
1646 </div>
1647 <div class="lh-crc">
1648 <div class="crc-initial-nav">Initial Navigation</div>
1649 <!-- stamp for each chain -->
1650 <template id="tmpl-lh-crc__chains">
1651 <div class="crc-node">
1652 <span class="crc-node__tree-marker">
1653
1654 </span>
1655 <span class="crc-node__tree-value">
1656 <span class="crc-node__tree-file"><!-- fill me: node.request.url.file --></span>
1657 <span class="crc-node__tree-hostname">(<!-- fill me: node.request.url.host -->)</span>
1658
1659 </span>
1660 </div>
1661 </template>
1662 </div>
1663 </div>
1664</template>
1665</div>
1666
1667 <main><!-- report populated here --></main>
1668
1669 <div id="lh-log"></div>
1670
1671 <script>/**
1672 * @license Copyright 2017 Google Inc. All Rights Reserved.
1673 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
1674 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
1675 */
1676'use strict';
1677
1678/* globals self URL */
1679
1680const ELLIPSIS = '\u2026';
1681const NBSP = '\xa0';
1682const PASS_THRESHOLD = 0.75;
1683
1684const RATINGS = {
1685 PASS: {label: 'pass', minScore: PASS_THRESHOLD},
1686 AVERAGE: {label: 'average', minScore: 0.45},
1687 FAIL: {label: 'fail'},
1688 ERROR: {label: 'error'},
1689};
1690
1691class Util {
1692 static get PASS_THRESHOLD() {
1693 return PASS_THRESHOLD;
1694 }
1695
1696 static get MS_DISPLAY_VALUE() {
1697 return `%10d${NBSP}ms`;
1698 }
1699
1700 /**
1701 * @param {string|Array<string|number>=} displayValue
1702 * @return {string}
1703 */
1704 static formatDisplayValue(displayValue) {
1705 if (typeof displayValue === 'string') return displayValue;
1706 if (!displayValue) return '';
1707
1708 const replacementRegex = /%([0-9]*(\.[0-9]+)?d|s)/;
1709 const template = /** @type {string} */ (displayValue[0]);
1710 if (typeof template !== 'string') {
1711 // First value should always be the format string, but we don't want to fail to build
1712 // a report, return a placeholder.
1713 return 'UNKNOWN';
1714 }
1715
1716 let output = template;
1717 for (const replacement of displayValue.slice(1)) {
1718 if (!replacementRegex.test(output)) {
1719 // eslint-disable-next-line no-console
1720 console.warn('Too many replacements given');
1721 break;
1722 }
1723
1724 output = output.replace(replacementRegex, match => {
1725 const granularity = Number(match.match(/[0-9.]+/)) || 1;
1726 return match === '%s' ?
1727 replacement.toLocaleString() :
1728 (Math.round(Number(replacement) / granularity) * granularity).toLocaleString();
1729 });
1730 }
1731
1732 if (replacementRegex.test(output)) {
1733 // eslint-disable-next-line no-console
1734 console.warn('Not enough replacements given');
1735 }
1736
1737 return output;
1738 }
1739
1740 /**
1741 * Used to determine if the "passed" for the purposes of showing up in the "failed" or "passed"
1742 * sections of the report.
1743 *
1744 * @param {{score: (number|null), scoreDisplayMode: string}} audit
1745 * @return {boolean}
1746 */
1747 static showAsPassed(audit) {
1748 switch (audit.scoreDisplayMode) {
1749 case 'manual':
1750 case 'not-applicable':
1751 return true;
1752 case 'error':
1753 case 'informative':
1754 return false;
1755 case 'numeric':
1756 case 'binary':
1757 default:
1758 // Numeric audits that are within PASS_THRESHOLD will still show up with failing.
1759 // For opportunities, we want to have them show up with other failing for contrast.
1760 // For diagnostics, we sort by score so they'll be lowest priority.
1761 return Number(audit.score) === 1;
1762 }
1763 }
1764
1765 /**
1766 * Convert a score to a rating label.
1767 * @param {number|null} score
1768 * @param {string=} scoreDisplayMode
1769 * @return {string}
1770 */
1771 static calculateRating(score, scoreDisplayMode) {
1772 // Handle edge cases first, manual and not applicable receive 'pass', errored audits receive 'error'
1773 if (scoreDisplayMode === 'manual' || scoreDisplayMode === 'not-applicable') {
1774 return RATINGS.PASS.label;
1775 } else if (scoreDisplayMode === 'error') {
1776 return RATINGS.ERROR.label;
1777 } else if (score === null) {
1778 return RATINGS.FAIL.label;
1779 }
1780
1781 // At this point, we're rating a standard binary/numeric audit
1782 let rating = RATINGS.FAIL.label;
1783 if (score >= RATINGS.PASS.minScore) {
1784 rating = RATINGS.PASS.label;
1785 } else if (score >= RATINGS.AVERAGE.minScore) {
1786 rating = RATINGS.AVERAGE.label;
1787 }
1788 return rating;
1789 }
1790
1791 /**
1792 * Format number.
1793 * @param {number} number
1794 * @param {number=} granularity Number of decimal places to include. Defaults to 0.1.
1795 * @return {string}
1796 */
1797 static formatNumber(number, granularity = 0.1) {
1798 const coarseValue = Math.round(number / granularity) * granularity;
1799 return coarseValue.toLocaleString();
1800 }
1801
1802 /**
1803 * @param {number} size
1804 * @param {number=} granularity Controls how coarse the displayed value is, defaults to .01
1805 * @return {string}
1806 */
1807 static formatBytesToKB(size, granularity = 0.1) {
1808 const kbs = (Math.round(size / 1024 / granularity) * granularity).toLocaleString();
1809 return `${kbs}${NBSP}KB`;
1810 }
1811
1812 /**
1813 * @param {number} ms
1814 * @param {number=} granularity Controls how coarse the displayed value is, defaults to 10
1815 * @return {string}
1816 */
1817 static formatMilliseconds(ms, granularity = 10) {
1818 const coarseTime = Math.round(ms / granularity) * granularity;
1819 return `${coarseTime.toLocaleString()}${NBSP}ms`;
1820 }
1821
1822 /**
1823 * @param {number} ms
1824 * @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
1825 * @return {string}
1826 */
1827 static formatSeconds(ms, granularity = 0.1) {
1828 const coarseTime = Math.round(ms / 1000 / granularity) * granularity;
1829 return `${coarseTime.toLocaleString()}${NBSP}s`;
1830 }
1831
1832 /**
1833 * Format time.
1834 * @param {string} date
1835 * @return {string}
1836 */
1837 static formatDateTime(date) {
1838 const options = {
1839 month: 'short', day: 'numeric', year: 'numeric',
1840 hour: 'numeric', minute: 'numeric', timeZoneName: 'short',
1841 };
1842 let formatter = new Intl.DateTimeFormat('en-US', options);
1843
1844 // Force UTC if runtime timezone could not be detected.
1845 // See https://github.com/GoogleChrome/lighthouse/issues/1056
1846 const tz = formatter.resolvedOptions().timeZone;
1847 if (!tz || tz.toLowerCase() === 'etc/unknown') {
1848 options.timeZone = 'UTC';
1849 formatter = new Intl.DateTimeFormat('en-US', options);
1850 }
1851 return formatter.format(new Date(date));
1852 }
1853 /**
1854 * Converts a time in milliseconds into a duration string, i.e. `1d 2h 13m 52s`
1855 * @param {number} timeInMilliseconds
1856 * @return {string}
1857 */
1858 static formatDuration(timeInMilliseconds) {
1859 let timeInSeconds = timeInMilliseconds / 1000;
1860 if (Math.round(timeInSeconds) === 0) {
1861 return 'None';
1862 }
1863
1864 /** @type {Array<string>} */
1865 const parts = [];
1866 const unitLabels = /** @type {Object<string, number>} */ ({
1867 d: 60 * 60 * 24,
1868 h: 60 * 60,
1869 m: 60,
1870 s: 1,
1871 });
1872
1873 Object.keys(unitLabels).forEach(label => {
1874 const unit = unitLabels[label];
1875 const numberOfUnits = Math.floor(timeInSeconds / unit);
1876 if (numberOfUnits > 0) {
1877 timeInSeconds -= numberOfUnits * unit;
1878 parts.push(`${numberOfUnits}\xa0${label}`);
1879 }
1880 });
1881
1882 return parts.join(' ');
1883 }
1884
1885 /**
1886 * @param {URL} parsedUrl
1887 * @param {{numPathParts?: number, preserveQuery?: boolean, preserveHost?: boolean}=} options
1888 * @return {string}
1889 */
1890 static getURLDisplayName(parsedUrl, options) {
1891 // Closure optional properties aren't optional in tsc, so fallback needs undefined values.
1892 options = options || {numPathParts: undefined, preserveQuery: undefined,
1893 preserveHost: undefined};
1894 const numPathParts = options.numPathParts !== undefined ? options.numPathParts : 2;
1895 const preserveQuery = options.preserveQuery !== undefined ? options.preserveQuery : true;
1896 const preserveHost = options.preserveHost || false;
1897
1898 let name;
1899
1900 if (parsedUrl.protocol === 'about:' || parsedUrl.protocol === 'data:') {
1901 // Handle 'about:*' and 'data:*' URLs specially since they have no path.
1902 name = parsedUrl.href;
1903 } else {
1904 name = parsedUrl.pathname;
1905 const parts = name.split('/').filter(part => part.length);
1906 if (numPathParts && parts.length > numPathParts) {
1907 name = ELLIPSIS + parts.slice(-1 * numPathParts).join('/');
1908 }
1909
1910 if (preserveHost) {
1911 name = `${parsedUrl.host}/${name.replace(/^\//, '')}`;
1912 }
1913 if (preserveQuery) {
1914 name = `${name}${parsedUrl.search}`;
1915 }
1916 }
1917
1918 const MAX_LENGTH = 64;
1919 // Always elide hexadecimal hash
1920 name = name.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g, `$1${ELLIPSIS}`);
1921 // Also elide other hash-like mixed-case strings
1922 name = name.replace(/([a-zA-Z0-9-_]{9})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9-_]{10,}/g,
1923 `$1${ELLIPSIS}`);
1924 // Also elide long number sequences
1925 name = name.replace(/(\d{3})\d{6,}/g, `$1${ELLIPSIS}`);
1926 // Merge any adjacent ellipses
1927 name = name.replace(/\u2026+/g, ELLIPSIS);
1928
1929 // Elide query params first
1930 if (name.length > MAX_LENGTH && name.includes('?')) {
1931 // Try to leave the first query parameter intact
1932 name = name.replace(/\?([^=]*)(=)?.*/, `?$1$2${ELLIPSIS}`);
1933
1934 // Remove it all if it's still too long
1935 if (name.length > MAX_LENGTH) {
1936 name = name.replace(/\?.*/, `?${ELLIPSIS}`);
1937 }
1938 }
1939
1940 // Elide too long names next
1941 if (name.length > MAX_LENGTH) {
1942 const dotIndex = name.lastIndexOf('.');
1943 if (dotIndex >= 0) {
1944 name = name.slice(0, MAX_LENGTH - 1 - (name.length - dotIndex)) +
1945 // Show file extension
1946 `${ELLIPSIS}${name.slice(dotIndex)}`;
1947 } else {
1948 name = name.slice(0, MAX_LENGTH - 1) + ELLIPSIS;
1949 }
1950 }
1951
1952 return name;
1953 }
1954
1955 /**
1956 * Split a URL into a file, hostname and origin for easy display.
1957 * @param {string} url
1958 * @return {{file: string, hostname: string, origin: string}}
1959 */
1960 static parseURL(url) {
1961 const parsedUrl = new URL(url);
1962 return {
1963 file: Util.getURLDisplayName(parsedUrl),
1964 hostname: parsedUrl.hostname,
1965 origin: parsedUrl.origin,
1966 };
1967 }
1968
1969 /**
1970 * @param {number} startTime
1971 * @param {number} endTime
1972 * @return {string}
1973 */
1974 static chainDuration(startTime, endTime) {
1975 return Util.formatNumber((endTime - startTime) * 1000);
1976 }
1977
1978 /**
1979 * @param {LH.Config.Settings} settings
1980 * @return {Array<{name: string, description: string}>}
1981 */
1982 static getEnvironmentDisplayValues(settings) {
1983 const emulationDesc = Util.getEmulationDescriptions(settings);
1984
1985 return [
1986 {
1987 name: 'Device',
1988 description: emulationDesc.deviceEmulation,
1989 },
1990 {
1991 name: 'Network throttling',
1992 description: emulationDesc.networkThrottling,
1993 },
1994 {
1995 name: 'CPU throttling',
1996 description: emulationDesc.cpuThrottling,
1997 },
1998 ];
1999 }
2000
2001 /**
2002 * @param {LH.Config.Settings} settings
2003 * @return {{deviceEmulation: string, networkThrottling: string, cpuThrottling: string, summary: string}}
2004 */
2005 static getEmulationDescriptions(settings) {
2006 let cpuThrottling;
2007 let networkThrottling;
2008 let summary;
2009
2010 const throttling = settings.throttling;
2011
2012 switch (settings.throttlingMethod) {
2013 case 'provided':
2014 cpuThrottling = 'Provided by environment';
2015 networkThrottling = 'Provided by environment';
2016 summary = 'No throttling applied';
2017 break;
2018 case 'devtools': {
2019 const {cpuSlowdownMultiplier, requestLatencyMs} = throttling;
2020 cpuThrottling = `${Util.formatNumber(cpuSlowdownMultiplier)}x slowdown (DevTools)`;
2021 networkThrottling = `${Util.formatNumber(requestLatencyMs)}${NBSP}ms HTTP RTT, ` +
2022 `${Util.formatNumber(throttling.downloadThroughputKbps)}${NBSP}Kbps down, ` +
2023 `${Util.formatNumber(throttling.uploadThroughputKbps)}${NBSP}Kbps up (DevTools)`;
2024 summary = 'Throttled Fast 3G network';
2025 break;
2026 }
2027 case 'simulate': {
2028 const {cpuSlowdownMultiplier, rttMs, throughputKbps} = throttling;
2029 cpuThrottling = `${Util.formatNumber(cpuSlowdownMultiplier)}x slowdown (Simulated)`;
2030 networkThrottling = `${Util.formatNumber(rttMs)}${NBSP}ms TCP RTT, ` +
2031 `${Util.formatNumber(throughputKbps)}${NBSP}Kbps throughput (Simulated)`;
2032 summary = 'Simulated Fast 3G network';
2033 break;
2034 }
2035 default:
2036 cpuThrottling = 'Unknown';
2037 networkThrottling = 'Unknown';
2038 summary = 'Unknown';
2039 }
2040
2041 const deviceEmulation = settings.disableDeviceEmulation ? 'No emulation' : 'Emulated Nexus 5X';
2042 return {
2043 deviceEmulation,
2044 cpuThrottling,
2045 networkThrottling,
2046 summary: `${deviceEmulation}, ${summary}`,
2047 };
2048 }
2049}
2050
2051if (typeof module !== 'undefined' && module.exports) {
2052 module.exports = Util;
2053} else {
2054 self.Util = Util;
2055}
2056;
2057/**
2058 * @license Copyright 2017 Google Inc. All Rights Reserved.
2059 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
2060 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
2061 */
2062'use strict';
2063
2064/* globals URL self */
2065
2066class DOM {
2067 /**
2068 * @param {Document} document
2069 */
2070 constructor(document) {
2071 /** @type {Document} */
2072 this._document = document;
2073 }
2074
2075 // TODO(bckenny): can pass along `createElement`'s inferred type
2076 /**
2077 * @param {string} name
2078 * @param {string=} className
2079 * @param {Object<string, (string|undefined)>=} attrs Attribute key/val pairs.
2080 * Note: if an attribute key has an undefined value, this method does not
2081 * set the attribute on the node.
2082 * @return {Element}
2083 */
2084 createElement(name, className, attrs = {}) {
2085 const element = this._document.createElement(name);
2086 if (className) {
2087 element.className = className;
2088 }
2089 Object.keys(attrs).forEach(key => {
2090 const value = attrs[key];
2091 if (typeof value !== 'undefined') {
2092 element.setAttribute(key, value);
2093 }
2094 });
2095 return element;
2096 }
2097
2098 /**
2099 * @return {DocumentFragment}
2100 */
2101 createFragment() {
2102 return this._document.createDocumentFragment();
2103 }
2104
2105 /**
2106 * @param {Element} parentElem
2107 * @param {string} elementName
2108 * @param {string=} className
2109 * @param {Object<string, (string|undefined)>=} attrs Attribute key/val pairs.
2110 * Note: if an attribute key has an undefined value, this method does not
2111 * set the attribute on the node.
2112 * @return {Element}
2113 */
2114 createChildOf(parentElem, elementName, className, attrs) {
2115 const element = this.createElement(elementName, className, attrs);
2116 parentElem.appendChild(element);
2117 return element;
2118 }
2119
2120 /**
2121 * @param {string} selector
2122 * @param {ParentNode} context
2123 * @return {DocumentFragment} A clone of the template content.
2124 * @throws {Error}
2125 */
2126 cloneTemplate(selector, context) {
2127 const template = /** @type {?HTMLTemplateElement} */ (context.querySelector(selector));
2128 if (!template) {
2129 throw new Error(`Template not found: template${selector}`);
2130 }
2131
2132 const clone = this._document.importNode(template.content, true);
2133
2134 // Prevent duplicate styles in the DOM. After a template has been stamped
2135 // for the first time, remove the clone's styles so they're not re-added.
2136 if (template.hasAttribute('data-stamped')) {
2137 this.findAll('style', clone).forEach(style => style.remove());
2138 }
2139 template.setAttribute('data-stamped', 'true');
2140
2141 return clone;
2142 }
2143
2144 /**
2145 * Resets the "stamped" state of the templates.
2146 */
2147 resetTemplates() {
2148 this.findAll('template[data-stamped]', this._document).forEach(t => {
2149 t.removeAttribute('data-stamped');
2150 });
2151 }
2152
2153 /**
2154 * @param {string} text
2155 * @return {Element}
2156 */
2157 convertMarkdownLinkSnippets(text) {
2158 const element = this.createElement('span');
2159
2160 // Split on markdown links (e.g. [some link](https://...)).
2161 const parts = text.split(/\[([^\]]*?)\]\((https?:\/\/.*?)\)/g);
2162
2163 while (parts.length) {
2164 // Pop off the same number of elements as there are capture groups.
2165 const [preambleText, linkText, linkHref] = parts.splice(0, 3);
2166 element.appendChild(this._document.createTextNode(preambleText));
2167
2168 // Append link if there are any.
2169 if (linkText && linkHref) {
2170 const a = /** @type {HTMLAnchorElement} */ (this.createElement('a'));
2171 a.rel = 'noopener';
2172 a.target = '_blank';
2173 a.textContent = linkText;
2174 a.href = (new URL(linkHref)).href;
2175 element.appendChild(a);
2176 }
2177 }
2178
2179 return element;
2180 }
2181
2182 /**
2183 * @param {string} text
2184 * @return {Element}
2185 */
2186 convertMarkdownCodeSnippets(text) {
2187 const element = this.createElement('span');
2188
2189 const parts = text.split(/`(.*?)`/g); // Split on markdown code slashes
2190 while (parts.length) {
2191 // Pop off the same number of elements as there are capture groups.
2192 const [preambleText, codeText] = parts.splice(0, 2);
2193 element.appendChild(this._document.createTextNode(preambleText));
2194 if (codeText) {
2195 const pre = /** @type {HTMLPreElement} */ (this.createElement('code'));
2196 pre.textContent = codeText;
2197 element.appendChild(pre);
2198 }
2199 }
2200
2201 return element;
2202 }
2203
2204 /**
2205 * @return {Document}
2206 */
2207 document() {
2208 return this._document;
2209 }
2210
2211 /**
2212 * TODO(paulirish): import and conditionally apply the DevTools frontend subclasses instead of this
2213 * @return {boolean}
2214 */
2215 isDevTools() {
2216 return !!this._document.querySelector('.lh-devtools');
2217 }
2218
2219 /**
2220 * Guaranteed context.querySelector. Always returns an element or throws if
2221 * nothing matches query.
2222 * @param {string} query
2223 * @param {ParentNode} context
2224 * @return {HTMLElement}
2225 */
2226 find(query, context) {
2227 /** @type {?HTMLElement} */
2228 const result = context.querySelector(query);
2229 if (result === null) {
2230 throw new Error(`query ${query} not found`);
2231 }
2232 return result;
2233 }
2234
2235 /**
2236 * Helper for context.querySelectorAll. Returns an Array instead of a NodeList.
2237 * @param {string} query
2238 * @param {ParentNode} context
2239 * @return {Array<HTMLElement>}
2240 */
2241 findAll(query, context) {
2242 return Array.from(context.querySelectorAll(query));
2243 }
2244}
2245
2246if (typeof module !== 'undefined' && module.exports) {
2247 module.exports = DOM;
2248} else {
2249 self.DOM = DOM;
2250}
2251;
2252/**
2253 * @license Copyright 2017 Google Inc. All Rights Reserved.
2254 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
2255 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
2256 */
2257'use strict';
2258
2259/* globals self CriticalRequestChainRenderer Util URL */
2260
2261/** @typedef {import('./dom.js')} DOM */
2262/** @typedef {import('./crc-details-renderer.js')} CRCDetailsJSON */
2263/** @typedef {LH.Result.Audit.OpportunityDetails} OpportunityDetails */
2264
2265/** @type {Array<string>} */
2266const URL_PREFIXES = ['http://', 'https://', 'data:'];
2267
2268class DetailsRenderer {
2269 /**
2270 * @param {DOM} dom
2271 */
2272 constructor(dom) {
2273 /** @type {DOM} */
2274 this._dom = dom;
2275 /** @type {ParentNode} */
2276 this._templateContext; // eslint-disable-line no-unused-expressions
2277 }
2278
2279 /**
2280 * @param {ParentNode} context
2281 */
2282 setTemplateContext(context) {
2283 this._templateContext = context;
2284 }
2285
2286 /**
2287 * @param {DetailsJSON|OpportunityDetails} details
2288 * @return {Element}
2289 */
2290 render(details) {
2291 switch (details.type) {
2292 case 'text':
2293 return this._renderText(/** @type {StringDetailsJSON} */ (details));
2294 case 'url':
2295 return this._renderTextURL(/** @type {StringDetailsJSON} */ (details));
2296 case 'bytes':
2297 return this._renderBytes(/** @type {NumericUnitDetailsJSON} */ (details));
2298 case 'ms':
2299 // eslint-disable-next-line max-len
2300 return this._renderMilliseconds(/** @type {NumericUnitDetailsJSON} */ (details));
2301 case 'link':
2302 // @ts-ignore - TODO(bckenny): Fix type hierarchy
2303 return this._renderLink(/** @type {LinkDetailsJSON} */ (details));
2304 case 'thumbnail':
2305 return this._renderThumbnail(/** @type {ThumbnailDetails} */ (details));
2306 case 'filmstrip':
2307 // @ts-ignore - TODO(bckenny): Fix type hierarchy
2308 return this._renderFilmstrip(/** @type {FilmstripDetails} */ (details));
2309 case 'table':
2310 // @ts-ignore - TODO(bckenny): Fix type hierarchy
2311 return this._renderTable(/** @type {TableDetailsJSON} */ (details));
2312 case 'code':
2313 return this._renderCode(/** @type {DetailsJSON} */ (details));
2314 case 'node':
2315 return this.renderNode(/** @type {NodeDetailsJSON} */(details));
2316 case 'criticalrequestchain':
2317 return CriticalRequestChainRenderer.render(this._dom, this._templateContext,
2318 // @ts-ignore - TODO(bckenny): Fix type hierarchy
2319 /** @type {CRCDetailsJSON} */ (details));
2320 case 'opportunity':
2321 // @ts-ignore - TODO(bckenny): Fix type hierarchy
2322 return this._renderOpportunityTable(details);
2323 default: {
2324 throw new Error(`Unknown type: ${details.type}`);
2325 }
2326 }
2327 }
2328
2329 /**
2330 * @param {{value: number, granularity?: number}} details
2331 * @return {Element}
2332 */
2333 _renderBytes(details) {
2334 // TODO: handle displayUnit once we have something other than 'kb'
2335 const value = Util.formatBytesToKB(details.value, details.granularity);
2336 return this._renderText({value});
2337 }
2338
2339 /**
2340 * @param {{value: number, granularity?: number, displayUnit?: string}} details
2341 * @return {Element}
2342 */
2343 _renderMilliseconds(details) {
2344 let value = Util.formatMilliseconds(details.value, details.granularity);
2345 if (details.displayUnit === 'duration') {
2346 value = Util.formatDuration(details.value);
2347 }
2348
2349 return this._renderText({value});
2350 }
2351
2352 /**
2353 * @param {{value: string}} text
2354 * @return {HTMLElement}
2355 */
2356 _renderTextURL(text) {
2357 const url = text.value;
2358
2359 let displayedPath;
2360 let displayedHost;
2361 let title;
2362 try {
2363 const parsed = Util.parseURL(url);
2364 displayedPath = parsed.file === '/' ? parsed.origin : parsed.file;
2365 displayedHost = parsed.file === '/' ? '' : `(${parsed.hostname})`;
2366 title = url;
2367 } catch (/** @type {!Error} */ e) {
2368 if (!e.name.startsWith('TypeError')) {
2369 throw e;
2370 }
2371 displayedPath = url;
2372 }
2373
2374 const element = /** @type {HTMLElement} */ (this._dom.createElement('div', 'lh-text__url'));
2375 element.appendChild(this._renderText({
2376 value: displayedPath,
2377 }));
2378
2379 if (displayedHost) {
2380 const hostElem = this._renderText({
2381 value: displayedHost,
2382 });
2383 hostElem.classList.add('lh-text__url-host');
2384 element.appendChild(hostElem);
2385 }
2386
2387 if (title) element.title = url;
2388 return element;
2389 }
2390
2391 /**
2392 * @param {LinkDetailsJSON} details
2393 * @return {Element}
2394 */
2395 _renderLink(details) {
2396 const allowedProtocols = ['https:', 'http:'];
2397 const url = new URL(details.url);
2398 if (!allowedProtocols.includes(url.protocol)) {
2399 // Fall back to just the link text if protocol not allowed.
2400 return this._renderText({
2401 value: details.text,
2402 });
2403 }
2404
2405 const a = /** @type {HTMLAnchorElement} */ (this._dom.createElement('a'));
2406 a.rel = 'noopener';
2407 a.target = '_blank';
2408 a.textContent = details.text;
2409 a.href = url.href;
2410
2411 return a;
2412 }
2413
2414 /**
2415 * @param {{value: string}} text
2416 * @return {Element}
2417 */
2418 _renderText(text) {
2419 const element = this._dom.createElement('div', 'lh-text');
2420 element.textContent = text.value;
2421 return element;
2422 }
2423
2424 /**
2425 * Create small thumbnail with scaled down image asset.
2426 * If the supplied details doesn't have an image/* mimeType, then an empty span is returned.
2427 * @param {{value: string}} details
2428 * @return {Element}
2429 */
2430 _renderThumbnail(details) {
2431 const element = /** @type {HTMLImageElement}*/ (this._dom.createElement('img', 'lh-thumbnail'));
2432 const strValue = details.value;
2433 element.src = strValue;
2434 element.title = strValue;
2435 element.alt = '';
2436 return element;
2437 }
2438
2439 /**
2440 * @param {TableDetailsJSON} details
2441 * @return {Element}
2442 */
2443 _renderTable(details) {
2444 if (!details.items.length) return this._dom.createElement('span');
2445
2446 const tableElem = this._dom.createElement('table', 'lh-table');
2447 const theadElem = this._dom.createChildOf(tableElem, 'thead');
2448 const theadTrElem = this._dom.createChildOf(theadElem, 'tr');
2449
2450 for (const heading of details.headings) {
2451 const itemType = heading.itemType || 'text';
2452 const classes = `lh-table-column--${itemType}`;
2453 this._dom.createChildOf(theadTrElem, 'th', classes).appendChild(this.render({
2454 type: 'text',
2455 value: heading.text || '',
2456 }));
2457 }
2458
2459 const tbodyElem = this._dom.createChildOf(tableElem, 'tbody');
2460 for (const row of details.items) {
2461 const rowElem = this._dom.createChildOf(tbodyElem, 'tr');
2462 for (const heading of details.headings) {
2463 const key = /** @type {keyof DetailsJSON} */ (heading.key);
2464 // TODO(bckenny): type should be naturally inferred here.
2465 const value = /** @type {number|string|DetailsJSON|undefined} */ (row[key]);
2466
2467 if (typeof value === 'undefined' || value === null) {
2468 this._dom.createChildOf(rowElem, 'td', 'lh-table-column--empty');
2469 continue;
2470 }
2471 // handle nested types like code blocks in table rows.
2472 // @ts-ignore - TODO(bckenny): narrow first
2473 if (value.type) {
2474 const valueAsDetails = /** @type {DetailsJSON} */ (value);
2475 const classes = `lh-table-column--${valueAsDetails.type}`;
2476 this._dom.createChildOf(rowElem, 'td', classes).appendChild(this.render(valueAsDetails));
2477 continue;
2478 }
2479
2480 // build new details item to render
2481 const item = {
2482 value: /** @type {number|string} */ (value),
2483 type: heading.itemType,
2484 displayUnit: heading.displayUnit,
2485 granularity: heading.granularity,
2486 };
2487
2488 /** @type {string|undefined} */
2489 // @ts-ignore - TODO(bckenny): handle with refactoring above
2490 const valueType = value.type;
2491 const classes = `lh-table-column--${valueType || heading.itemType}`;
2492 this._dom.createChildOf(rowElem, 'td', classes).appendChild(this.render(item));
2493 }
2494 }
2495 return tableElem;
2496 }
2497
2498 /**
2499 * TODO(bckenny): migrate remaining table rendering to this function, then rename
2500 * back to _renderTable and replace the original.
2501 * @param {OpportunityDetails} details
2502 * @return {Element}
2503 */
2504 _renderOpportunityTable(details) {
2505 if (!details.items.length) return this._dom.createElement('span');
2506
2507 const tableElem = this._dom.createElement('table', 'lh-table');
2508 const theadElem = this._dom.createChildOf(tableElem, 'thead');
2509 const theadTrElem = this._dom.createChildOf(theadElem, 'tr');
2510
2511 for (const heading of details.headings) {
2512 const valueType = heading.valueType || 'text';
2513 const classes = `lh-table-column--${valueType}`;
2514 const labelEl = this._dom.createElement('div', 'lh-text');
2515 labelEl.textContent = heading.label;
2516 this._dom.createChildOf(theadTrElem, 'th', classes).appendChild(labelEl);
2517 }
2518
2519 const tbodyElem = this._dom.createChildOf(tableElem, 'tbody');
2520 for (const row of details.items) {
2521 const rowElem = this._dom.createChildOf(tbodyElem, 'tr');
2522 for (const heading of details.headings) {
2523 const key = /** @type {keyof LH.Result.Audit.OpportunityDetailsItem} */ (heading.key);
2524 const value = row[key];
2525
2526 if (typeof value === 'undefined' || value === null) {
2527 this._dom.createChildOf(rowElem, 'td', 'lh-table-column--empty');
2528 continue;
2529 }
2530
2531 const valueType = heading.valueType;
2532 let itemElement;
2533
2534 // TODO(bckenny): as we add more table types, split out into _renderTableItem fn.
2535 switch (valueType) {
2536 case 'url': {
2537 // Fall back to <pre> rendering if not actually a URL.
2538 const strValue = /** @type {string} */ (value);
2539 if (URL_PREFIXES.some(prefix => strValue.startsWith(prefix))) {
2540 itemElement = this._renderTextURL({value: strValue});
2541 } else {
2542 const codeValue = /** @type {(number|string|undefined)} */ (value);
2543 itemElement = this._renderCode({value: codeValue});
2544 }
2545 break;
2546 }
2547 case 'timespanMs': {
2548 const numValue = /** @type {number} */ (value);
2549 itemElement = this._renderMilliseconds({value: numValue});
2550 break;
2551 }
2552 case 'bytes': {
2553 const numValue = /** @type {number} */ (value);
2554 itemElement = this._renderBytes({value: numValue, granularity: 1});
2555 break;
2556 }
2557 case 'thumbnail': {
2558 const strValue = /** @type {string} */ (value);
2559 itemElement = this._renderThumbnail({value: strValue});
2560 break;
2561 }
2562 default: {
2563 throw new Error(`Unknown valueType: ${valueType}`);
2564 }
2565 }
2566
2567 const classes = `lh-table-column--${valueType}`;
2568 this._dom.createChildOf(rowElem, 'td', classes).appendChild(itemElement);
2569 }
2570 }
2571 return tableElem;
2572 }
2573
2574 /**
2575 * @param {NodeDetailsJSON} item
2576 * @return {Element}
2577 * @protected
2578 */
2579 renderNode(item) {
2580 const element = /** @type {HTMLSpanElement} */ (this._dom.createElement('span', 'lh-node'));
2581 if (item.snippet) {
2582 element.textContent = item.snippet;
2583 }
2584 if (item.selector) {
2585 element.title = item.selector;
2586 }
2587 if (item.path) element.setAttribute('data-path', item.path);
2588 if (item.selector) element.setAttribute('data-selector', item.selector);
2589 if (item.snippet) element.setAttribute('data-snippet', item.snippet);
2590 return element;
2591 }
2592
2593 /**
2594 * @param {FilmstripDetails} details
2595 * @return {Element}
2596 */
2597 _renderFilmstrip(details) {
2598 const filmstripEl = this._dom.createElement('div', 'lh-filmstrip');
2599
2600 for (const thumbnail of details.items) {
2601 const frameEl = this._dom.createChildOf(filmstripEl, 'div', 'lh-filmstrip__frame');
2602 this._dom.createChildOf(frameEl, 'img', 'lh-filmstrip__thumbnail', {
2603 src: `data:image/jpeg;base64,${thumbnail.data}`,
2604 alt: `Screenshot`,
2605 });
2606 }
2607 return filmstripEl;
2608 }
2609
2610 /**
2611 * @param {{value?: string|number}} details
2612 * @return {Element}
2613 */
2614 _renderCode(details) {
2615 const pre = this._dom.createElement('pre', 'lh-code');
2616 pre.textContent = /** @type {string} */ (details.value);
2617 return pre;
2618 }
2619}
2620
2621if (typeof module !== 'undefined' && module.exports) {
2622 module.exports = DetailsRenderer;
2623} else {
2624 self.DetailsRenderer = DetailsRenderer;
2625}
2626
2627// TODO, what's the diff between DetailsJSON and NumericUnitDetailsJSON?
2628/**
2629 * @typedef {{
2630 type: string,
2631 value: (string|number|undefined),
2632 granularity?: number,
2633 displayUnit?: string
2634 }} DetailsJSON
2635 */
2636
2637/**
2638 * @typedef {{
2639 type: string,
2640 value: string,
2641 granularity?: number,
2642 displayUnit?: string,
2643 }} StringDetailsJSON
2644 */
2645
2646/**
2647 * @typedef {{
2648 type: string,
2649 value: number,
2650 granularity?: number,
2651 displayUnit?: string,
2652 }} NumericUnitDetailsJSON
2653 */
2654
2655/**
2656 * @typedef {{
2657 type: string,
2658 path?: string,
2659 selector?: string,
2660 snippet?: string
2661 }} NodeDetailsJSON
2662 */
2663
2664/**
2665 * @typedef {{
2666 itemType: string,
2667 key: string,
2668 text?: string,
2669 granularity?: number,
2670 displayUnit?: string,
2671 }} TableHeaderJSON
2672 */
2673
2674/** @typedef {{
2675 type: string,
2676 items: Array<DetailsJSON>,
2677 headings: Array<TableHeaderJSON>
2678 }} TableDetailsJSON
2679 */
2680
2681/** @typedef {{
2682 type: string,
2683 value: string,
2684 }} ThumbnailDetails
2685 */
2686
2687/** @typedef {{
2688 type: string,
2689 text: string,
2690 url: string
2691 }} LinkDetailsJSON
2692 */
2693
2694/** @typedef {{
2695 type: string,
2696 scale: number,
2697 items: Array<{timing: number, timestamp: number, data: string}>,
2698 }} FilmstripDetails
2699 */
2700;
2701/**
2702 * @license Copyright 2017 Google Inc. All Rights Reserved.
2703 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
2704 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
2705 */
2706'use strict';
2707
2708/**
2709 * @fileoverview This file contains helpers for constructing and rendering the
2710 * critical request chains network tree.
2711 */
2712
2713/* globals self Util */
2714
2715/** @typedef {import('./dom.js')} DOM */
2716
2717class CriticalRequestChainRenderer {
2718 /**
2719 * Create render context for critical-request-chain tree display.
2720 * @param {LH.Audit.SimpleCriticalRequestNode} tree
2721 * @return {{tree: LH.Audit.SimpleCriticalRequestNode, startTime: number, transferSize: number}}
2722 */
2723 static initTree(tree) {
2724 let startTime = 0;
2725 const rootNodes = Object.keys(tree);
2726 if (rootNodes.length > 0) {
2727 const node = tree[rootNodes[0]];
2728 startTime = node.request.startTime;
2729 }
2730
2731 return {tree, startTime, transferSize: 0};
2732 }
2733
2734 /**
2735 * Helper to create context for each critical-request-chain node based on its
2736 * parent. Calculates if this node is the last child, whether it has any
2737 * children itself and what the tree looks like all the way back up to the root,
2738 * so the tree markers can be drawn correctly.
2739 * @param {LH.Audit.SimpleCriticalRequestNode} parent
2740 * @param {string} id
2741 * @param {number} startTime
2742 * @param {number} transferSize
2743 * @param {Array<boolean>=} treeMarkers
2744 * @param {boolean=} parentIsLastChild
2745 * @return {CRCSegment}
2746 */
2747 static createSegment(parent, id, startTime, transferSize, treeMarkers, parentIsLastChild) {
2748 const node = parent[id];
2749 const siblings = Object.keys(parent);
2750 const isLastChild = siblings.indexOf(id) === (siblings.length - 1);
2751 const hasChildren = Object.keys(node.children).length > 0;
2752
2753 // Copy the tree markers so that we don't change by reference.
2754 const newTreeMarkers = Array.isArray(treeMarkers) ? treeMarkers.slice(0) : [];
2755
2756 // Add on the new entry.
2757 if (typeof parentIsLastChild !== 'undefined') {
2758 newTreeMarkers.push(!parentIsLastChild);
2759 }
2760
2761 return {
2762 node,
2763 isLastChild,
2764 hasChildren,
2765 startTime,
2766 transferSize: transferSize + node.request.transferSize,
2767 treeMarkers: newTreeMarkers,
2768 };
2769 }
2770
2771 /**
2772 * Creates the DOM for a tree segment.
2773 * @param {DOM} dom
2774 * @param {DocumentFragment} tmpl
2775 * @param {CRCSegment} segment
2776 * @return {Node}
2777 */
2778 static createChainNode(dom, tmpl, segment) {
2779 const chainsEl = dom.cloneTemplate('#tmpl-lh-crc__chains', tmpl);
2780
2781 // Hovering over request shows full URL.
2782 dom.find('.crc-node', chainsEl).setAttribute('title', segment.node.request.url);
2783
2784 const treeMarkeEl = dom.find('.crc-node__tree-marker', chainsEl);
2785
2786 // Construct lines and add spacers for sub requests.
2787 segment.treeMarkers.forEach(separator => {
2788 if (separator) {
2789 treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker vert'));
2790 treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker'));
2791 } else {
2792 treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker'));
2793 treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker'));
2794 }
2795 });
2796
2797 if (segment.isLastChild) {
2798 treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker up-right'));
2799 treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker right'));
2800 } else {
2801 treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker vert-right'));
2802 treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker right'));
2803 }
2804
2805 if (segment.hasChildren) {
2806 treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker horiz-down'));
2807 } else {
2808 treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker right'));
2809 }
2810
2811 // Fill in url, host, and request size information.
2812 const {file, hostname} = Util.parseURL(segment.node.request.url);
2813 const treevalEl = dom.find('.crc-node__tree-value', chainsEl);
2814 dom.find('.crc-node__tree-file', treevalEl).textContent = `${file}`;
2815 dom.find('.crc-node__tree-hostname', treevalEl).textContent = hostname ? `(${hostname})` : '';
2816
2817 if (!segment.hasChildren) {
2818 const span = dom.createElement('span', 'crc-node__chain-duration');
2819 span.textContent = ' - ' + Util.chainDuration(
2820 segment.node.request.startTime, segment.node.request.endTime) + 'ms, ';
2821 const span2 = dom.createElement('span', 'crc-node__chain-duration');
2822 span2.textContent = Util.formatBytesToKB(segment.node.request.transferSize, 0.01);
2823
2824 treevalEl.appendChild(span);
2825 treevalEl.appendChild(span2);
2826 }
2827
2828 return chainsEl;
2829 }
2830
2831 /**
2832 * Recursively builds a tree from segments.
2833 * @param {DOM} dom
2834 * @param {DocumentFragment} tmpl
2835 * @param {CRCSegment} segment
2836 * @param {Element} elem Parent element.
2837 * @param {CRCDetailsJSON} details
2838 */
2839 static buildTree(dom, tmpl, segment, elem, details) {
2840 elem.appendChild(CriticalRequestChainRenderer.createChainNode(dom, tmpl, segment));
2841
2842 for (const key of Object.keys(segment.node.children)) {
2843 const childSegment = CriticalRequestChainRenderer.createSegment(segment.node.children, key,
2844 segment.startTime, segment.transferSize, segment.treeMarkers, segment.isLastChild);
2845 CriticalRequestChainRenderer.buildTree(dom, tmpl, childSegment, elem, details);
2846 }
2847 }
2848
2849 /**
2850 * @param {DOM} dom
2851 * @param {ParentNode} templateContext
2852 * @param {CRCDetailsJSON} details
2853 * @return {Element}
2854 */
2855 static render(dom, templateContext, details) {
2856 const tmpl = dom.cloneTemplate('#tmpl-lh-crc', templateContext);
2857 const containerEl = dom.find('.lh-crc', tmpl);
2858
2859 // Fill in top summary.
2860 dom.find('.lh-crc__longest_duration', tmpl).textContent =
2861 Util.formatNumber(details.longestChain.duration) + 'ms';
2862 dom.find('.lh-crc__longest_length', tmpl).textContent = details.longestChain.length.toString();
2863 dom.find('.lh-crc__longest_transfersize', tmpl).textContent =
2864 Util.formatBytesToKB(details.longestChain.transferSize);
2865
2866 // Construct visual tree.
2867 const root = CriticalRequestChainRenderer.initTree(details.chains);
2868 for (const key of Object.keys(root.tree)) {
2869 const segment = CriticalRequestChainRenderer.createSegment(root.tree, key,
2870 root.startTime, root.transferSize);
2871 CriticalRequestChainRenderer.buildTree(dom, tmpl, segment, containerEl, details);
2872 }
2873
2874 return dom.find('.lh-crc-container', tmpl);
2875 }
2876}
2877
2878// Allow Node require()'ing.
2879if (typeof module !== 'undefined' && module.exports) {
2880 module.exports = CriticalRequestChainRenderer;
2881} else {
2882 self.CriticalRequestChainRenderer = CriticalRequestChainRenderer;
2883}
2884
2885/** @typedef {{
2886 type: string,
2887 header: {text: string},
2888 longestChain: {duration: number, length: number, transferSize: number},
2889 chains: LH.Audit.SimpleCriticalRequestNode
2890 }} CRCDetailsJSON
2891 */
2892
2893/** @typedef {{
2894 node: LH.Audit.SimpleCriticalRequestNode[string],
2895 isLastChild: boolean,
2896 hasChildren: boolean,
2897 startTime: number,
2898 transferSize: number,
2899 treeMarkers: Array<boolean>
2900 }} CRCSegment
2901 */
2902;
2903/**
2904 * @license Copyright 2017 Google Inc. All Rights Reserved.
2905 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
2906 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
2907 */
2908'use strict';
2909
2910/* global URL */
2911
2912/**
2913 * @fileoverview
2914 * @suppress {reportUnknownTypes}
2915 */
2916
2917/**
2918 * Generate a filenamePrefix of hostname_YYYY-MM-DD_HH-MM-SS
2919 * Date/time uses the local timezone, however Node has unreliable ICU
2920 * support, so we must construct a YYYY-MM-DD date format manually. :/
2921 * @param {{finalUrl: string, fetchTime: string}} lhr
2922 * @return {string}
2923 */
2924function getFilenamePrefix(lhr) {
2925 const hostname = new (getUrlConstructor())(lhr.finalUrl).hostname;
2926 const date = (lhr.fetchTime && new Date(lhr.fetchTime)) || new Date();
2927
2928 const timeStr = date.toLocaleTimeString('en-US', {hour12: false});
2929 const dateParts = date.toLocaleDateString('en-US', {
2930 year: 'numeric', month: '2-digit', day: '2-digit',
2931 }).split('/');
2932 // @ts-ignore - parts exists
2933 dateParts.unshift(dateParts.pop());
2934 const dateStr = dateParts.join('-');
2935
2936 const filenamePrefix = `${hostname}_${dateStr}_${timeStr}`;
2937 // replace characters that are unfriendly to filenames
2938 return filenamePrefix.replace(/[/?<>\\:*|":]/g, '-');
2939}
2940
2941function getUrlConstructor() {
2942 if (typeof module !== 'undefined' && module.exports) {
2943 return require('./url-shim');
2944 } else {
2945 return URL;
2946 }
2947}
2948
2949// @ts-ignore - suppress `module` error for type-checking in a browser context.
2950if (typeof module !== 'undefined' && module.exports) {
2951 module.exports = {getFilenamePrefix};
2952}
2953;
2954/**
2955 * @license Copyright 2017 Google Inc. All Rights Reserved.
2956 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
2957 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
2958 */
2959'use strict';
2960
2961/**
2962 * Logs messages via a UI butter.
2963 */
2964class Logger {
2965 /**
2966 * @param {Element} element
2967 */
2968 constructor(element) {
2969 /** @type {Element} */
2970 this.el = element;
2971 this._id = undefined;
2972 }
2973
2974 /**
2975 * Shows a butter bar.
2976 * @param {string} msg The message to show.
2977 * @param {boolean=} autoHide True to hide the message after a duration.
2978 * Default is true.
2979 */
2980 log(msg, autoHide = true) {
2981 this._id && clearTimeout(this._id);
2982
2983 this.el.textContent = msg;
2984 this.el.classList.add('show');
2985 if (autoHide) {
2986 this._id = setTimeout(_ => {
2987 this.el.classList.remove('show');
2988 }, 7000);
2989 }
2990 }
2991
2992 /**
2993 * @param {string} msg
2994 */
2995 warn(msg) {
2996 this.log('Warning: ' + msg);
2997 }
2998
2999 /**
3000 * @param {string} msg
3001 */
3002 error(msg) {
3003 this.log(msg);
3004
3005 // Rethrow to make sure it's auditable as an error, but in a setTimeout so page
3006 // recovers gracefully and user can try loading a report again.
3007 setTimeout(_ => {
3008 throw new Error(msg);
3009 }, 0);
3010 }
3011
3012 /**
3013 * Explicitly hides the butter bar.
3014 */
3015 hide() {
3016 this._id && clearTimeout(this._id);
3017 this.el.classList.remove('show');
3018 }
3019}
3020
3021if (typeof module !== 'undefined' && module.exports) {
3022 module.exports = Logger;
3023}
3024;
3025/**
3026 * @license Copyright 2017 Google Inc. All Rights Reserved.
3027 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
3028 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
3029 */
3030'use strict';
3031
3032/**
3033 * @fileoverview Adds export button, print, and other dynamic functionality to
3034 * the report.
3035 */
3036
3037/* globals self URL Blob CustomEvent getFilenamePrefix window */
3038
3039/** @typedef {import('./dom.js')} DOM */
3040
3041class ReportUIFeatures {
3042 /**
3043 * @param {DOM} dom
3044 */
3045 constructor(dom) {
3046 /** @type {LH.ReportResult} */
3047 this.json; // eslint-disable-line no-unused-expressions
3048 /** @type {DOM} */
3049 this._dom = dom;
3050 /** @type {Document} */
3051 this._document = this._dom.document();
3052 /** @type {boolean} */
3053 this._copyAttempt = false;
3054 /** @type {HTMLElement} */
3055 this.exportButton; // eslint-disable-line no-unused-expressions
3056 /** @type {HTMLElement} */
3057 this.headerSticky; // eslint-disable-line no-unused-expressions
3058 /** @type {HTMLElement} */
3059 this.headerBackground; // eslint-disable-line no-unused-expressions
3060 /** @type {HTMLElement} */
3061 this.lighthouseIcon; // eslint-disable-line no-unused-expressions
3062 /** @type {!HTMLElement} */
3063 this.scoresWrapperBg; // eslint-disable-line no-unused-expressions
3064 /** @type {!HTMLElement} */
3065 this.productInfo; // eslint-disable-line no-unused-expressions
3066 /** @type {HTMLElement} */
3067 this.toolbar; // eslint-disable-line no-unused-expressions
3068 /** @type {HTMLElement} */
3069 this.toolbarMetadata; // eslint-disable-line no-unused-expressions
3070 /** @type {HTMLElement} */
3071 this.env; // eslint-disable-line no-unused-expressions
3072 /** @type {number} */
3073 this.headerOverlap = 0;
3074 /** @type {number} */
3075 this.headerHeight = 0;
3076 /** @type {number} */
3077 this.latestKnownScrollY = 0;
3078 /** @type {boolean} */
3079 this.isAnimatingHeader = false;
3080
3081 this.onMediaQueryChange = this.onMediaQueryChange.bind(this);
3082 this.onCopy = this.onCopy.bind(this);
3083 this.onExportButtonClick = this.onExportButtonClick.bind(this);
3084 this.onExport = this.onExport.bind(this);
3085 this.onKeyDown = this.onKeyDown.bind(this);
3086 this.printShortCutDetect = this.printShortCutDetect.bind(this);
3087 this.onScroll = this.onScroll.bind(this);
3088 this.onChevronClick = this.onChevronClick.bind(this);
3089 }
3090
3091 /**
3092 * Adds export button, print, and other functionality to the report. The method
3093 * should be called whenever the report needs to be re-rendered.
3094 * @param {LH.ReportResult} report
3095 */
3096 initFeatures(report) {
3097 if (this._dom.isDevTools()) return;
3098
3099 this.json = report;
3100 this._setupMediaQueryListeners();
3101 this._setupExportButton();
3102 this._setUpCollapseDetailsAfterPrinting();
3103 this._setupHeaderAnimation();
3104 this._resetUIState();
3105 this._document.addEventListener('keydown', this.printShortCutDetect);
3106 // @ts-ignore - tsc thinks document can't listen for `copy`
3107 this._document.addEventListener('copy', this.onCopy);
3108 }
3109
3110 /**
3111 * Fires a custom DOM event on target.
3112 * @param {string} name Name of the event.
3113 * @param {Node=} target DOM node to fire the event on.
3114 * @param {*=} detail Custom data to include.
3115 */
3116 _fireEventOn(name, target = this._document, detail) {
3117 const event = new CustomEvent(name, detail ? {detail} : undefined);
3118 target.dispatchEvent(event);
3119 }
3120
3121 _setupMediaQueryListeners() {
3122 const mediaQuery = self.matchMedia('(max-width: 600px)');
3123 mediaQuery.addListener(this.onMediaQueryChange);
3124 // Ensure the handler is called on init
3125 this.onMediaQueryChange(mediaQuery);
3126 }
3127
3128 /**
3129 * Handle media query change events.
3130 * @param {MediaQueryList} mql
3131 */
3132 onMediaQueryChange(mql) {
3133 const root = this._dom.find('.lh-root', this._document);
3134 root.classList.toggle('lh-narrow', mql.matches);
3135 }
3136
3137 _setupExportButton() {
3138 this.exportButton = this._dom.find('.lh-export__button', this._document);
3139 this.exportButton.addEventListener('click', this.onExportButtonClick);
3140
3141 const dropdown = this._dom.find('.lh-export__dropdown', this._document);
3142 dropdown.addEventListener('click', this.onExport);
3143 }
3144
3145 _setupHeaderAnimation() {
3146 const scoresWrapper = this._dom.find('.lh-scores-wrapper', this._document);
3147 this.headerOverlap = /** @type {number} */
3148 // @ts-ignore - TODO: move off CSSOM to support other browsers
3149 (scoresWrapper.computedStyleMap().get('margin-top').value);
3150
3151 this.headerSticky = this._dom.find('.lh-header-sticky', this._document);
3152 this.headerBackground = this._dom.find('.lh-header-bg', this._document);
3153 this.lighthouseIcon = this._dom.find('.lh-lighthouse', this._document);
3154 this.scoresWrapperBg = this._dom.find('.lh-scores-wrapper__background', this._document);
3155 this.productInfo = this._dom.find('.lh-product-info', this._document);
3156 this.toolbar = this._dom.find('.lh-toolbar', this._document);
3157 this.toolbarMetadata = this._dom.find('.lh-toolbar__metadata', this._document);
3158
3159 // @ts-ignore - TODO: move off CSSOM to support other browsers
3160 this.headerHeight = this.headerBackground.computedStyleMap().get('height').value;
3161
3162 this._document.addEventListener('scroll', this.onScroll, {passive: true});
3163
3164 const toolbarChevron = this._dom.find('.lh-toggle-arrow', this.toolbar);
3165 toolbarChevron.addEventListener('click', this.onChevronClick);
3166 }
3167
3168 /**
3169 * Handle copy events.
3170 * @param {ClipboardEvent} e
3171 */
3172 onCopy(e) {
3173 // Only handle copy button presses (e.g. ignore the user copying page text).
3174 if (this._copyAttempt) {
3175 // We want to write our own data to the clipboard, not the user's text selection.
3176 e.preventDefault();
3177 e.clipboardData.setData('text/plain', JSON.stringify(this.json, null, 2));
3178
3179 this._fireEventOn('lh-log', this._document, {
3180 cmd: 'log', msg: 'Report JSON copied to clipboard',
3181 });
3182 }
3183
3184 this._copyAttempt = false;
3185 }
3186
3187 /**
3188 * Copies the report JSON to the clipboard (if supported by the browser).
3189 */
3190 onCopyButtonClick() {
3191 this._fireEventOn('lh-analytics', this._document, {
3192 cmd: 'send',
3193 fields: {hitType: 'event', eventCategory: 'report', eventAction: 'copy'},
3194 });
3195
3196 try {
3197 if (this._document.queryCommandSupported('copy')) {
3198 this._copyAttempt = true;
3199
3200 // Note: In Safari 10.0.1, execCommand('copy') returns true if there's
3201 // a valid text selection on the page. See http://caniuse.com/#feat=clipboard.
3202 if (!this._document.execCommand('copy')) {
3203 this._copyAttempt = false; // Prevent event handler from seeing this as a copy attempt.
3204
3205 this._fireEventOn('lh-log', this._document, {
3206 cmd: 'warn', msg: 'Your browser does not support copy to clipboard.',
3207 });
3208 }
3209 }
3210 } catch (/** @type {Error} */ e) {
3211 this._copyAttempt = false;
3212 this._fireEventOn('lh-log', this._document, {cmd: 'log', msg: e.message});
3213 }
3214 }
3215
3216 onScroll() {
3217 this.latestKnownScrollY = window.scrollY;
3218
3219 if (!this.isAnimatingHeader) {
3220 window.requestAnimationFrame(this.animateHeader.bind(this));
3221 }
3222 this.isAnimatingHeader = true;
3223 }
3224
3225 onChevronClick() {
3226 const toggle = this._dom.find('.lh-config__settings-toggle', this._document);
3227
3228 if (toggle.hasAttribute('open')) {
3229 toggle.removeAttribute('open');
3230 } else {
3231 toggle.setAttribute('open', 'true');
3232 }
3233 }
3234
3235 animateHeader() {
3236 const collapsedHeaderHeight = 50;
3237 const heightDiff = this.headerHeight - collapsedHeaderHeight + this.headerOverlap;
3238 const scrollPct = Math.min(1,
3239 this.latestKnownScrollY / (this.headerHeight - collapsedHeaderHeight));
3240
3241 const scoresContainer = /** @type {HTMLElement} */ (this.scoresWrapperBg.parentElement);
3242
3243 this.headerSticky.style.transform = `translateY(${heightDiff * scrollPct * -1}px)`;
3244 this.headerBackground.style.transform = `translateY(${scrollPct * this.headerOverlap}px)`;
3245 this.lighthouseIcon.style.transform =
3246 `translate3d(calc(var(--report-content-width) / 2),` +
3247 ` calc(-100% - ${scrollPct * this.headerOverlap * -1}px), 0) scale(${1 - scrollPct})`;
3248 this.lighthouseIcon.style.opacity = Math.max(0, 1 - scrollPct).toString();
3249
3250 // Switch up the score background & shadows
3251 this.scoresWrapperBg.style.opacity = (1 - scrollPct).toString();
3252 this.scoresWrapperBg.style.transform = `scaleY(${1 - scrollPct * 0.2})`;
3253 const scoreShadow = this._dom.find('.lh-scores-wrapper__shadow', scoresContainer);
3254 scoreShadow.style.opacity = scrollPct.toString();
3255 scoreShadow.style.transform = `scaleY(${1 - scrollPct * 0.2})`;
3256
3257 // Fade & move the scorescale
3258 const scoreScalePositionDelta = 32;
3259 const scoreScale = this._dom.find('.lh-scorescale', scoresContainer);
3260 scoreScale.style.opacity = `${1 - scrollPct}`;
3261 scoreScale.style.transform = `translateY(${scrollPct * -scoreScalePositionDelta}px)`;
3262
3263 // Move the toolbar & export
3264 this.toolbar.style.transform = `translateY(${heightDiff * scrollPct}px)`;
3265 const exportParent = this.exportButton.parentElement;
3266 if (exportParent) {
3267 exportParent.style.transform = `translateY(${heightDiff * scrollPct}px)`;
3268 }
3269 this.exportButton.style.transform = `scale(${1 - 0.2 * scrollPct})`;
3270 // Start showing the productinfo when we are at the 50% mark of our animation
3271 const opacity = scrollPct < 0.5 ? 0 : (scrollPct - 0.5) * 2;
3272 this.productInfo.style.opacity = this.toolbarMetadata.style.opacity = opacity.toString();
3273
3274 this.isAnimatingHeader = false;
3275 }
3276
3277 closeExportDropdown() {
3278 this.exportButton.classList.remove('active');
3279 }
3280
3281 /**
3282 * Click handler for export button.
3283 * @param {Event} e
3284 */
3285 onExportButtonClick(e) {
3286 e.preventDefault();
3287 const el = /** @type {Element} */ (e.target);
3288 el.classList.toggle('active');
3289 this._document.addEventListener('keydown', this.onKeyDown);
3290 }
3291
3292 /**
3293 * Resets the state of page before capturing the page for export.
3294 * When the user opens the exported HTML page, certain UI elements should
3295 * be in their closed state (not opened) and the templates should be unstamped.
3296 */
3297 _resetUIState() {
3298 this.closeExportDropdown();
3299 this._dom.resetTemplates();
3300 }
3301
3302 /**
3303 * Handler for "export as" button.
3304 * @param {Event} e
3305 */
3306 onExport(e) {
3307 e.preventDefault();
3308
3309 const el = /** @type {?Element} */ (e.target);
3310
3311 if (!el || !el.hasAttribute('data-action')) {
3312 return;
3313 }
3314
3315 switch (el.getAttribute('data-action')) {
3316 case 'copy':
3317 this.onCopyButtonClick();
3318 break;
3319 case 'print-summary':
3320 this.collapseAllDetails();
3321 this.closeExportDropdown();
3322 self.print();
3323 break;
3324 case 'print-expanded':
3325 this.expandAllDetails();
3326 this.closeExportDropdown();
3327 self.print();
3328 break;
3329 case 'save-json': {
3330 const jsonStr = JSON.stringify(this.json, null, 2);
3331 this._saveFile(new Blob([jsonStr], {type: 'application/json'}));
3332 break;
3333 }
3334 case 'save-html': {
3335 const htmlStr = this.getReportHtml();
3336 try {
3337 this._saveFile(new Blob([htmlStr], {type: 'text/html'}));
3338 } catch (/** @type {Error} */ e) {
3339 this._fireEventOn('lh-log', this._document, {
3340 cmd: 'error', msg: 'Could not export as HTML. ' + e.message,
3341 });
3342 }
3343 break;
3344 }
3345 case 'open-viewer': {
3346 const viewerPath = '/lighthouse/viewer/';
3347 ReportUIFeatures.openTabAndSendJsonReport(this.json, viewerPath);
3348 break;
3349 }
3350 case 'save-gist': {
3351 this.saveAsGist();
3352 break;
3353 }
3354 }
3355
3356 this.closeExportDropdown();
3357 this._document.removeEventListener('keydown', this.onKeyDown);
3358 }
3359
3360 /**
3361 * Keydown handler for the document.
3362 * @param {KeyboardEvent} e
3363 */
3364 onKeyDown(e) {
3365 if (e.keyCode === 27) { // ESC
3366 this.closeExportDropdown();
3367 }
3368 }
3369
3370 /**
3371 * Opens a new tab to the online viewer and sends the local page's JSON results
3372 * to the online viewer using postMessage.
3373 * @param {LH.ReportResult} reportJson
3374 * @param {string} viewerPath
3375 * @protected
3376 */
3377 static openTabAndSendJsonReport(reportJson, viewerPath) {
3378 const VIEWER_ORIGIN = 'https://googlechrome.github.io';
3379 // Chrome doesn't allow us to immediately postMessage to a popup right
3380 // after it's created. Normally, we could also listen for the popup window's
3381 // load event, however it is cross-domain and won't fire. Instead, listen
3382 // for a message from the target app saying "I'm open".
3383 const json = reportJson;
3384 window.addEventListener('message', function msgHandler(/** @type {Event} */ e) {
3385 const messageEvent = /** @type {MessageEvent} */ (e);
3386 if (messageEvent.origin !== VIEWER_ORIGIN) {
3387 return;
3388 }
3389 if (popup && messageEvent.data.opened) {
3390 popup.postMessage({lhresults: json}, VIEWER_ORIGIN);
3391 window.removeEventListener('message', msgHandler);
3392 }
3393 });
3394
3395 // The popup's window.name is keyed by version+url+fetchTime, so we reuse/select tabs correctly
3396 // @ts-ignore - If this is a v2 LHR, use old `generatedTime`.
3397 const fallbackFetchTime = /** @type {string} */ (json.generatedTime);
3398 const fetchTime = json.fetchTime || fallbackFetchTime;
3399 const windowName = `${json.lighthouseVersion}-${json.requestedUrl}-${fetchTime}`;
3400 const popup = window.open(`${VIEWER_ORIGIN}${viewerPath}`, windowName);
3401 }
3402
3403 /**
3404 * Expands audit details when user prints via keyboard shortcut.
3405 * @param {KeyboardEvent} e
3406 */
3407 printShortCutDetect(e) {
3408 if ((e.ctrlKey || e.metaKey) && e.keyCode === 80) { // Ctrl+P
3409 this.closeExportDropdown();
3410 }
3411 }
3412
3413 /**
3414 * Expands all audit `<details>`.
3415 * Ideally, a print stylesheet could take care of this, but CSS has no way to
3416 * open a `<details>` element.
3417 */
3418 expandAllDetails() {
3419 const details = /** @type {Array<HTMLDetailsElement>} */ (this._dom.findAll(
3420 '.lh-categories details', this._document));
3421 details.map(detail => detail.open = true);
3422 }
3423
3424 /**
3425 * Collapses all audit `<details>`.
3426 * open a `<details>` element.
3427 */
3428 collapseAllDetails() {
3429 const details = /** @type {Array<HTMLDetailsElement>} */ (this._dom.findAll(
3430 '.lh-categories details', this._document));
3431 details.map(detail => detail.open = false);
3432 }
3433
3434 /**
3435 * Sets up listeners to collapse audit `<details>` when the user closes the
3436 * print dialog, all `<details>` are collapsed.
3437 */
3438 _setUpCollapseDetailsAfterPrinting() {
3439 // FF and IE implement these old events.
3440 if ('onbeforeprint' in self) {
3441 self.addEventListener('afterprint', this.collapseAllDetails);
3442 } else {
3443 const win = /** @type {Window} */ (self);
3444 // Note: FF implements both window.onbeforeprint and media listeners. However,
3445 // it doesn't matchMedia doesn't fire when matching 'print'.
3446 win.matchMedia('print').addListener(mql => {
3447 if (mql.matches) {
3448 this.expandAllDetails();
3449 } else {
3450 this.collapseAllDetails();
3451 }
3452 });
3453 }
3454 }
3455
3456 /**
3457 * Returns the html that recreates this report.
3458 * @return {string}
3459 * @protected
3460 */
3461 getReportHtml() {
3462 this._resetUIState();
3463 return this._document.documentElement.outerHTML;
3464 }
3465
3466 /**
3467 * Save json as a gist. Unimplemented in base UI features.
3468 * @protected
3469 */
3470 saveAsGist() {
3471 throw new Error('Cannot save as gist from base report');
3472 }
3473
3474 /**
3475 * Downloads a file (blob) using a[download].
3476 * @param {Blob|File} blob The file to save.
3477 * @private
3478 */
3479 _saveFile(blob) {
3480 const filename = getFilenamePrefix({
3481 finalUrl: this.json.finalUrl,
3482 fetchTime: this.json.fetchTime,
3483 });
3484
3485 const ext = blob.type.match('json') ? '.json' : '.html';
3486 const href = URL.createObjectURL(blob);
3487
3488 const a = /** @type {HTMLAnchorElement} */ (this._dom.createElement('a'));
3489 a.download = `${filename}${ext}`;
3490 a.href = href;
3491 this._document.body.appendChild(a); // Firefox requires anchor to be in the DOM.
3492 a.click();
3493
3494 // cleanup.
3495 this._document.body.removeChild(a);
3496 setTimeout(_ => URL.revokeObjectURL(href), 500);
3497 }
3498}
3499
3500if (typeof module !== 'undefined' && module.exports) {
3501 module.exports = ReportUIFeatures;
3502} else {
3503 self.ReportUIFeatures = ReportUIFeatures;
3504}
3505;
3506/**
3507 * @license Copyright 2017 Google Inc. All Rights Reserved.
3508 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
3509 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
3510 */
3511'use strict';
3512
3513/* globals self, Util */
3514
3515/** @typedef {import('./dom.js')} DOM */
3516/** @typedef {import('./report-renderer.js')} ReportRenderer */
3517/** @typedef {import('./details-renderer.js')} DetailsRenderer */
3518/** @typedef {import('./util.js')} Util */
3519
3520class CategoryRenderer {
3521 /**
3522 * @param {DOM} dom
3523 * @param {DetailsRenderer} detailsRenderer
3524 */
3525 constructor(dom, detailsRenderer) {
3526 /** @type {DOM} */
3527 this.dom = dom;
3528 /** @type {DetailsRenderer} */
3529 this.detailsRenderer = detailsRenderer;
3530 /** @type {ParentNode} */
3531 this.templateContext = this.dom.document();
3532
3533 this.detailsRenderer.setTemplateContext(this.templateContext);
3534 }
3535
3536 /**
3537 * @param {LH.ReportResult.AuditRef} audit
3538 * @param {number} index
3539 * @return {Element}
3540 */
3541 renderAudit(audit, index) {
3542 const tmpl = this.dom.cloneTemplate('#tmpl-lh-audit', this.templateContext);
3543 return this.populateAuditValues(audit, index, tmpl);
3544 }
3545
3546 /**
3547 * Populate an DOM tree with audit details. Used by renderAudit and renderOpportunity
3548 * @param {LH.ReportResult.AuditRef} audit
3549 * @param {number} index
3550 * @param {DocumentFragment} tmpl
3551 * @return {Element}
3552 */
3553 populateAuditValues(audit, index, tmpl) {
3554 const auditEl = this.dom.find('.lh-audit', tmpl);
3555 auditEl.id = audit.result.id;
3556 const scoreDisplayMode = audit.result.scoreDisplayMode;
3557
3558 if (audit.result.displayValue) {
3559 const displayValue = Util.formatDisplayValue(audit.result.displayValue);
3560 this.dom.find('.lh-audit__display-text', auditEl).textContent = displayValue;
3561 }
3562
3563 const titleEl = this.dom.find('.lh-audit__title', auditEl);
3564 titleEl.appendChild(this.dom.convertMarkdownCodeSnippets(audit.result.title));
3565 this.dom.find('.lh-audit__description', auditEl)
3566 .appendChild(this.dom.convertMarkdownLinkSnippets(audit.result.description));
3567
3568 const header = /** @type {HTMLDetailsElement} */ (this.dom.find('details', auditEl));
3569 if (audit.result.details && audit.result.details.type) {
3570 const elem = this.detailsRenderer.render(audit.result.details);
3571 elem.classList.add('lh-details');
3572 header.appendChild(elem);
3573 }
3574 this.dom.find('.lh-audit__index', auditEl).textContent = `${index + 1}`;
3575
3576 // Add chevron SVG to the end of the summary
3577 this.dom.find('.lh-chevron-container', auditEl).appendChild(this._createChevron());
3578 this._setRatingClass(auditEl, audit.result.score, scoreDisplayMode);
3579
3580 if (audit.result.scoreDisplayMode === 'error') {
3581 auditEl.classList.add(`lh-audit--error`);
3582 const textEl = this.dom.find('.lh-audit__display-text', auditEl);
3583 textEl.textContent = 'Error!';
3584 textEl.classList.add('tooltip-boundary');
3585 const tooltip = this.dom.createChildOf(textEl, 'div', 'tooltip tooltip--error');
3586 tooltip.textContent = audit.result.errorMessage || 'Report error: no audit information';
3587 } else if (audit.result.explanation) {
3588 const explEl = this.dom.createChildOf(titleEl, 'div', 'lh-audit-explanation');
3589 explEl.textContent = audit.result.explanation;
3590 }
3591 const warnings = audit.result.warnings;
3592 if (!warnings || warnings.length === 0) return auditEl;
3593
3594 // Add list of warnings or singular warning
3595 const warningsEl = this.dom.createChildOf(titleEl, 'div', 'lh-warnings');
3596 if (warnings.length === 1) {
3597 warningsEl.textContent = `Warning: ${warnings.join('')}`;
3598 } else {
3599 warningsEl.textContent = 'Warnings: ';
3600 const warningsUl = this.dom.createChildOf(warningsEl, 'ul');
3601 for (const warning of warnings) {
3602 const item = this.dom.createChildOf(warningsUl, 'li');
3603 item.textContent = warning;
3604 }
3605 }
3606 return auditEl;
3607 }
3608
3609 /**
3610 * @return {HTMLElement}
3611 */
3612 _createChevron() {
3613 const chevronTmpl = this.dom.cloneTemplate('#tmpl-lh-chevron', this.templateContext);
3614 const chevronEl = this.dom.find('.lh-chevron', chevronTmpl);
3615 return chevronEl;
3616 }
3617
3618 /**
3619 * @param {Element} element DOM node to populate with values.
3620 * @param {number|null} score
3621 * @param {string} scoreDisplayMode
3622 * @return {Element}
3623 */
3624 _setRatingClass(element, score, scoreDisplayMode) {
3625 const rating = Util.calculateRating(score, scoreDisplayMode);
3626 element.classList.add(`lh-audit--${rating}`, `lh-audit--${scoreDisplayMode}`);
3627 return element;
3628 }
3629
3630 /**
3631 * @param {LH.ReportResult.Category} category
3632 * @return {Element}
3633 */
3634 renderCategoryHeader(category) {
3635 const tmpl = this.dom.cloneTemplate('#tmpl-lh-category-header', this.templateContext);
3636
3637 const gaugeContainerEl = this.dom.find('.lh-score__gauge', tmpl);
3638 const gaugeEl = this.renderScoreGauge(category);
3639 gaugeContainerEl.appendChild(gaugeEl);
3640
3641 this.dom.find('.lh-category-header__title', tmpl).appendChild(
3642 this.dom.convertMarkdownCodeSnippets(category.title));
3643 if (category.description) {
3644 const descEl = this.dom.convertMarkdownLinkSnippets(category.description);
3645 this.dom.find('.lh-category-header__description', tmpl).appendChild(descEl);
3646 }
3647
3648 return /** @type {Element} */ (tmpl.firstElementChild);
3649 }
3650
3651 /**
3652 * Renders the group container for a group of audits. Individual audit elements can be added
3653 * directly to the returned element.
3654 * @param {LH.Result.ReportGroup} group
3655 * @param {{expandable: boolean, itemCount?: number}} opts
3656 * @return {Element}
3657 */
3658 renderAuditGroup(group, opts) {
3659 const expandable = opts.expandable;
3660 const groupEl = this.dom.createElement(expandable ? 'details' : 'div', 'lh-audit-group');
3661 const summmaryEl = this.dom.createChildOf(groupEl, 'summary', 'lh-audit-group__summary');
3662 const headerEl = this.dom.createChildOf(summmaryEl, 'div', 'lh-audit-group__header');
3663 const itemCountEl = this.dom.createChildOf(summmaryEl, 'div', 'lh-audit-group__itemcount');
3664 if (expandable) {
3665 const chevronEl = summmaryEl.appendChild(this._createChevron());
3666 chevronEl.title = 'Show audits';
3667 }
3668
3669 if (group.description) {
3670 const auditGroupDescription = this.dom.createElement('div', 'lh-audit-group__description');
3671 auditGroupDescription.appendChild(this.dom.convertMarkdownLinkSnippets(group.description));
3672 groupEl.appendChild(auditGroupDescription);
3673 }
3674 headerEl.textContent = group.title;
3675
3676 if (opts.itemCount) {
3677 itemCountEl.textContent = `${opts.itemCount} audits`;
3678 }
3679 return groupEl;
3680 }
3681
3682 /**
3683 * Find the total number of audits contained within a section.
3684 * Accounts for nested subsections like Accessibility.
3685 * @param {Array<Element>} elements
3686 * @return {number}
3687 */
3688 _getTotalAuditsLength(elements) {
3689 // Create a scratch element to append sections to so we can reuse querySelectorAll().
3690 const scratch = this.dom.createElement('div');
3691 elements.forEach(function(element) {
3692 scratch.appendChild(element);
3693 });
3694 const subAudits = scratch.querySelectorAll('.lh-audit');
3695 if (subAudits.length) {
3696 return subAudits.length;
3697 } else {
3698 return elements.length;
3699 }
3700 }
3701
3702 /**
3703 * @param {Array<Element>} elements
3704 * @return {Element}
3705 */
3706 _renderFailedAuditsSection(elements) {
3707 const failedElem = this.dom.createElement('div');
3708 failedElem.classList.add('lh-failed-audits');
3709 elements.forEach(elem => failedElem.appendChild(elem));
3710 return failedElem;
3711 }
3712
3713 /**
3714 * @param {Array<Element>} elements
3715 * @return {Element}
3716 */
3717 renderPassedAuditsSection(elements) {
3718 const passedElem = this.renderAuditGroup({
3719 title: `Passed audits`,
3720 }, {expandable: true, itemCount: this._getTotalAuditsLength(elements)});
3721 passedElem.classList.add('lh-passed-audits');
3722 elements.forEach(elem => passedElem.appendChild(elem));
3723 return passedElem;
3724 }
3725
3726 /**
3727 * @param {Array<Element>} elements
3728 * @return {Element}
3729 */
3730 _renderNotApplicableAuditsSection(elements) {
3731 const notApplicableElem = this.renderAuditGroup({
3732 title: `Not applicable`,
3733 }, {expandable: true, itemCount: this._getTotalAuditsLength(elements)});
3734 notApplicableElem.classList.add('lh-audit-group--not-applicable');
3735 elements.forEach(elem => notApplicableElem.appendChild(elem));
3736 return notApplicableElem;
3737 }
3738
3739 /**
3740 * @param {Array<LH.ReportResult.AuditRef>} manualAudits
3741 * @param {string} [manualDescription]
3742 * @return {Element}
3743 */
3744 _renderManualAudits(manualAudits, manualDescription) {
3745 const group = {title: 'Additional items to manually check', description: manualDescription};
3746 const auditGroupElem = this.renderAuditGroup(group,
3747 {expandable: true, itemCount: manualAudits.length});
3748 auditGroupElem.classList.add('lh-audit-group--manual');
3749 manualAudits.forEach((audit, i) => {
3750 auditGroupElem.appendChild(this.renderAudit(audit, i));
3751 });
3752 return auditGroupElem;
3753 }
3754
3755 /**
3756 * @param {ParentNode} context
3757 */
3758 setTemplateContext(context) {
3759 this.templateContext = context;
3760 this.detailsRenderer.setTemplateContext(context);
3761 }
3762
3763 /**
3764 * @param {LH.ReportResult.Category} category
3765 * @return {DocumentFragment}
3766 */
3767 renderScoreGauge(category) {
3768 const tmpl = this.dom.cloneTemplate('#tmpl-lh-gauge', this.templateContext);
3769 const wrapper = /** @type {HTMLAnchorElement} */ (this.dom.find('.lh-gauge__wrapper', tmpl));
3770 wrapper.href = `#${category.id}`;
3771 wrapper.classList.add(`lh-gauge__wrapper--${Util.calculateRating(category.score)}`);
3772
3773 // Cast `null` to 0
3774 const numericScore = Number(category.score);
3775 const gauge = this.dom.find('.lh-gauge', tmpl);
3776 // 329 is ~= 2 * Math.PI * gauge radius (53)
3777 // https://codepen.io/xgad/post/svg-radial-progress-meters
3778 // score of 50: `stroke-dasharray: 164.5 329`;
3779 /** @type {?SVGCircleElement} */
3780 const gaugeArc = gauge.querySelector('.lh-gauge-arc');
3781 if (gaugeArc) {
3782 gaugeArc.style.strokeDasharray = `${numericScore * 329} 329`;
3783 }
3784
3785 const scoreOutOf100 = Math.round(numericScore * 100);
3786 const percentageEl = this.dom.find('.lh-gauge__percentage', tmpl);
3787 percentageEl.textContent = scoreOutOf100.toString();
3788 if (category.score === null) {
3789 percentageEl.textContent = '?';
3790 percentageEl.title = 'Errors occurred while auditing';
3791 }
3792
3793 this.dom.find('.lh-gauge__label', tmpl).textContent = category.title;
3794 return tmpl;
3795 }
3796
3797 /**
3798 * @param {LH.ReportResult.Category} category
3799 * @param {Object<string, LH.Result.ReportGroup>} [groupDefinitions]
3800 * @return {Element}
3801 */
3802 render(category, groupDefinitions) {
3803 const element = this.dom.createElement('div', 'lh-category');
3804 this.createPermalinkSpan(element, category.id);
3805 element.appendChild(this.renderCategoryHeader(category));
3806
3807 const auditRefs = category.auditRefs;
3808 const manualAudits = auditRefs.filter(audit => audit.result.scoreDisplayMode === 'manual');
3809 const nonManualAudits = auditRefs.filter(audit => !manualAudits.includes(audit));
3810
3811 /** @type {Object<string, {passed: Array<LH.ReportResult.AuditRef>, failed: Array<LH.ReportResult.AuditRef>, notApplicable: Array<LH.ReportResult.AuditRef>}>} */
3812 const auditsGroupedByGroup = {};
3813 const auditsUngrouped = {passed: [], failed: [], notApplicable: []};
3814
3815 nonManualAudits.forEach(auditRef => {
3816 let group;
3817
3818 if (auditRef.group) {
3819 const groupId = auditRef.group;
3820
3821 if (auditsGroupedByGroup[groupId]) {
3822 group = auditsGroupedByGroup[groupId];
3823 } else {
3824 group = {passed: [], failed: [], notApplicable: []};
3825 auditsGroupedByGroup[groupId] = group;
3826 }
3827 } else {
3828 group = auditsUngrouped;
3829 }
3830
3831 if (auditRef.result.scoreDisplayMode === 'not-applicable') {
3832 group.notApplicable.push(auditRef);
3833 } else if (Util.showAsPassed(auditRef.result)) {
3834 group.passed.push(auditRef);
3835 } else {
3836 group.failed.push(auditRef);
3837 }
3838 });
3839
3840 const failedElements = /** @type {Array<Element>} */ ([]);
3841 const passedElements = /** @type {Array<Element>} */ ([]);
3842 const notApplicableElements = /** @type {Array<Element>} */ ([]);
3843
3844 auditsUngrouped.failed.forEach((audit, i) => failedElements.push(this.renderAudit(audit, i)));
3845 auditsUngrouped.passed.forEach((audit, i) => passedElements.push(this.renderAudit(audit, i)));
3846 auditsUngrouped.notApplicable.forEach((audit, i) => notApplicableElements.push(
3847 this.renderAudit(audit, i)));
3848
3849 Object.keys(auditsGroupedByGroup).forEach(groupId => {
3850 if (!groupDefinitions) return; // We never reach here if there aren't groups, but TSC needs convincing
3851
3852 const group = groupDefinitions[groupId];
3853 const groups = auditsGroupedByGroup[groupId];
3854
3855 if (groups.failed.length) {
3856 const auditGroupElem = this.renderAuditGroup(group, {expandable: false});
3857 groups.failed.forEach((item, i) => auditGroupElem.appendChild(this.renderAudit(item, i)));
3858 auditGroupElem.classList.add('lh-audit-group--unadorned');
3859 failedElements.push(auditGroupElem);
3860 }
3861
3862 if (groups.passed.length) {
3863 const auditGroupElem = this.renderAuditGroup(group, {expandable: true});
3864 groups.passed.forEach((item, i) => auditGroupElem.appendChild(this.renderAudit(item, i)));
3865 auditGroupElem.classList.add('lh-audit-group--unadorned');
3866 passedElements.push(auditGroupElem);
3867 }
3868
3869 if (groups.notApplicable.length) {
3870 const auditGroupElem = this.renderAuditGroup(group, {expandable: true});
3871 groups.notApplicable.forEach((item, i) =>
3872 auditGroupElem.appendChild(this.renderAudit(item, i)));
3873 auditGroupElem.classList.add('lh-audit-group--unadorned');
3874 notApplicableElements.push(auditGroupElem);
3875 }
3876 });
3877
3878 if (failedElements.length) {
3879 const failedElem = this._renderFailedAuditsSection(failedElements);
3880 element.appendChild(failedElem);
3881 }
3882
3883 if (manualAudits.length) {
3884 const manualEl = this._renderManualAudits(manualAudits, category.manualDescription);
3885 element.appendChild(manualEl);
3886 }
3887
3888 if (passedElements.length) {
3889 const passedElem = this.renderPassedAuditsSection(passedElements);
3890 element.appendChild(passedElem);
3891 }
3892
3893 if (notApplicableElements.length) {
3894 const notApplicableElem = this._renderNotApplicableAuditsSection(notApplicableElements);
3895 element.appendChild(notApplicableElem);
3896 }
3897
3898 return element;
3899 }
3900
3901 /**
3902 * Create a non-semantic span used for hash navigation of categories
3903 * @param {Element} element
3904 * @param {string} id
3905 */
3906 createPermalinkSpan(element, id) {
3907 const permalinkEl = this.dom.createChildOf(element, 'span', 'lh-permalink');
3908 permalinkEl.id = id;
3909 }
3910}
3911
3912if (typeof module !== 'undefined' && module.exports) {
3913 module.exports = CategoryRenderer;
3914} else {
3915 self.CategoryRenderer = CategoryRenderer;
3916}
3917;
3918/**
3919 * @license Copyright 2018 Google Inc. All Rights Reserved.
3920 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
3921 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
3922 */
3923'use strict';
3924
3925/* globals self, Util, CategoryRenderer */
3926
3927/** @typedef {import('./dom.js')} DOM */
3928/** @typedef {import('./details-renderer.js').FilmstripDetails} FilmstripDetails */
3929/** @typedef {LH.Result.Audit.OpportunityDetails} OpportunityDetails */
3930
3931class PerformanceCategoryRenderer extends CategoryRenderer {
3932 /**
3933 * @param {LH.ReportResult.AuditRef} audit
3934 * @return {Element}
3935 */
3936 _renderMetric(audit) {
3937 const tmpl = this.dom.cloneTemplate('#tmpl-lh-metric', this.templateContext);
3938 const element = this.dom.find('.lh-metric', tmpl);
3939 element.id = audit.result.id;
3940 const rating = Util.calculateRating(audit.result.score, audit.result.scoreDisplayMode);
3941 element.classList.add(`lh-metric--${rating}`);
3942
3943 const titleEl = this.dom.find('.lh-metric__title', tmpl);
3944 titleEl.textContent = audit.result.title;
3945
3946 const valueEl = this.dom.find('.lh-metric__value', tmpl);
3947 valueEl.textContent = Util.formatDisplayValue(audit.result.displayValue);
3948
3949 const descriptionEl = this.dom.find('.lh-metric__description', tmpl);
3950 descriptionEl.appendChild(this.dom.convertMarkdownLinkSnippets(audit.result.description));
3951
3952 if (audit.result.scoreDisplayMode === 'error') {
3953 descriptionEl.textContent = '';
3954 valueEl.textContent = 'Error!';
3955 const tooltip = this.dom.createChildOf(descriptionEl, 'span');
3956 tooltip.textContent = audit.result.errorMessage || 'Report error: no metric information';
3957 }
3958
3959 return element;
3960 }
3961
3962 /**
3963 * @param {LH.ReportResult.AuditRef} audit
3964 * @param {number} index
3965 * @param {number} scale
3966 * @return {Element}
3967 */
3968 _renderOpportunity(audit, index, scale) {
3969 const oppTmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity', this.templateContext);
3970 const element = this.populateAuditValues(audit, index, oppTmpl);
3971 element.id = audit.result.id;
3972
3973 if (!audit.result.details || audit.result.scoreDisplayMode === 'error') {
3974 return element;
3975 }
3976 // TODO(bckenny): remove cast when details is fully discriminated based on `type`.
3977 const details = /** @type {OpportunityDetails} */ (audit.result.details);
3978 if (details.type !== 'opportunity') {
3979 return element;
3980 }
3981
3982 // Overwrite the displayValue with opportunity's wastedMs
3983 const displayEl = this.dom.find('.lh-audit__display-text', element);
3984 const sparklineWidthPct = `${details.overallSavingsMs / scale * 100}%`;
3985 this.dom.find('.lh-sparkline__bar', element).style.width = sparklineWidthPct;
3986 displayEl.textContent = Util.formatSeconds(details.overallSavingsMs, 0.01);
3987
3988 // Set [title] tooltips
3989 if (audit.result.displayValue) {
3990 const displayValue = Util.formatDisplayValue(audit.result.displayValue);
3991 this.dom.find('.lh-load-opportunity__sparkline', element).title = displayValue;
3992 displayEl.title = displayValue;
3993 }
3994
3995 return element;
3996 }
3997
3998 /**
3999 * Get an audit's wastedMs to sort the opportunity by, and scale the sparkline width
4000 * Opportunties with an error won't have a details object, so MIN_VALUE is returned to keep any
4001 * erroring opportunities last in sort order.
4002 * @param {LH.ReportResult.AuditRef} audit
4003 * @return {number}
4004 */
4005 _getWastedMs(audit) {
4006 if (audit.result.details && audit.result.details.type === 'opportunity') {
4007 // TODO(bckenny): remove cast when details is fully discriminated based on `type`.
4008 const details = /** @type {OpportunityDetails} */ (audit.result.details);
4009 if (typeof details.overallSavingsMs !== 'number') {
4010 throw new Error('non-opportunity details passed to _getWastedMs');
4011 }
4012 return details.overallSavingsMs;
4013 } else {
4014 return Number.MIN_VALUE;
4015 }
4016 }
4017
4018 /**
4019 * @param {LH.ReportResult.Category} category
4020 * @param {Object<string, LH.Result.ReportGroup>} groups
4021 * @return {Element}
4022 * @override
4023 */
4024 render(category, groups) {
4025 const element = this.dom.createElement('div', 'lh-category');
4026 this.createPermalinkSpan(element, category.id);
4027 element.appendChild(this.renderCategoryHeader(category));
4028
4029 // Metrics
4030 const metricAudits = category.auditRefs.filter(audit => audit.group === 'metrics');
4031 const metricAuditsEl = this.renderAuditGroup(groups['metrics'], {expandable: false});
4032
4033 const keyMetrics = metricAudits.filter(a => a.weight >= 3);
4034 const otherMetrics = metricAudits.filter(a => a.weight < 3);
4035
4036 const metricsBoxesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metric-container');
4037 const metricsColumn1El = this.dom.createChildOf(metricsBoxesEl, 'div', 'lh-metric-column');
4038 const metricsColumn2El = this.dom.createChildOf(metricsBoxesEl, 'div', 'lh-metric-column');
4039
4040 keyMetrics.forEach(item => {
4041 metricsColumn1El.appendChild(this._renderMetric(item));
4042 });
4043 otherMetrics.forEach(item => {
4044 metricsColumn2El.appendChild(this._renderMetric(item));
4045 });
4046 const estValuesEl = this.dom.createChildOf(metricsColumn2El, 'div',
4047 'lh-metrics__disclaimer lh-metrics__disclaimer');
4048 estValuesEl.textContent = 'Values are estimated and may vary.';
4049
4050 metricAuditsEl.classList.add('lh-audit-group--metrics');
4051 element.appendChild(metricAuditsEl);
4052
4053 // Filmstrip
4054 const timelineEl = this.dom.createChildOf(element, 'div', 'lh-filmstrip-container');
4055 const thumbnailAudit = category.auditRefs.find(audit => audit.id === 'screenshot-thumbnails');
4056 const thumbnailResult = thumbnailAudit && thumbnailAudit.result;
4057 if (thumbnailResult && thumbnailResult.details) {
4058 timelineEl.id = thumbnailResult.id;
4059 const filmstripEl = this.detailsRenderer.render(thumbnailResult.details);
4060 timelineEl.appendChild(filmstripEl);
4061 }
4062
4063 // Opportunities
4064 const opportunityAudits = category.auditRefs
4065 .filter(audit => audit.group === 'load-opportunities' && !Util.showAsPassed(audit.result))
4066 .sort((auditA, auditB) => this._getWastedMs(auditB) - this._getWastedMs(auditA));
4067
4068 if (opportunityAudits.length) {
4069 // Scale the sparklines relative to savings, minimum 2s to not overstate small savings
4070 const minimumScale = 2000;
4071 const wastedMsValues = opportunityAudits.map(audit => this._getWastedMs(audit));
4072 const maxWaste = Math.max(...wastedMsValues);
4073 const scale = Math.max(Math.ceil(maxWaste / 1000) * 1000, minimumScale);
4074 const groupEl = this.renderAuditGroup(groups['load-opportunities'], {expandable: false});
4075 const tmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity-header', this.templateContext);
4076 const headerEl = this.dom.find('.lh-load-opportunity__header', tmpl);
4077 groupEl.appendChild(headerEl);
4078 opportunityAudits.forEach((item, i) =>
4079 groupEl.appendChild(this._renderOpportunity(item, i, scale)));
4080 groupEl.classList.add('lh-audit-group--opportunities');
4081 element.appendChild(groupEl);
4082 }
4083
4084 // Diagnostics
4085 const diagnosticAudits = category.auditRefs
4086 .filter(audit => audit.group === 'diagnostics' && !Util.showAsPassed(audit.result))
4087 .sort((a, b) => {
4088 const scoreA = a.result.scoreDisplayMode === 'informative' ? 100 : Number(a.result.score);
4089 const scoreB = b.result.scoreDisplayMode === 'informative' ? 100 : Number(b.result.score);
4090 return scoreA - scoreB;
4091 });
4092
4093 if (diagnosticAudits.length) {
4094 const groupEl = this.renderAuditGroup(groups['diagnostics'], {expandable: false});
4095 diagnosticAudits.forEach((item, i) => groupEl.appendChild(this.renderAudit(item, i)));
4096 groupEl.classList.add('lh-audit-group--diagnostics');
4097 element.appendChild(groupEl);
4098 }
4099
4100 // Passed audits
4101 const passedElements = category.auditRefs
4102 .filter(audit => (audit.group === 'load-opportunities' || audit.group === 'diagnostics') &&
4103 Util.showAsPassed(audit.result))
4104 .map((audit, i) => this.renderAudit(audit, i));
4105
4106 if (!passedElements.length) return element;
4107
4108 const passedElem = this.renderPassedAuditsSection(passedElements);
4109 element.appendChild(passedElem);
4110 return element;
4111 }
4112}
4113
4114if (typeof module !== 'undefined' && module.exports) {
4115 module.exports = PerformanceCategoryRenderer;
4116} else {
4117 self.PerformanceCategoryRenderer = PerformanceCategoryRenderer;
4118}
4119;
4120/**
4121 * @license Copyright 2017 Google Inc. All Rights Reserved.
4122 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4123 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
4124 */
4125'use strict';
4126
4127/**
4128 * @fileoverview The entry point for rendering the Lighthouse report based on the JSON output.
4129 * This file is injected into the report HTML along with the JSON report.
4130 *
4131 * Dummy text for ensuring report robustness: \u003c/script> pre$`post %%LIGHTHOUSE_JSON%%
4132 */
4133
4134/** @typedef {import('./dom.js')} DOM */
4135/** @typedef {import('./details-renderer.js').DetailsJSON} DetailsJSON */
4136
4137/* globals self, Util, DetailsRenderer, CategoryRenderer, PerformanceCategoryRenderer */
4138
4139class ReportRenderer {
4140 /**
4141 * @param {DOM} dom
4142 */
4143 constructor(dom) {
4144 /** @type {DOM} */
4145 this._dom = dom;
4146 /** @type {ParentNode} */
4147 this._templateContext = this._dom.document();
4148 }
4149
4150 /**
4151 * @param {LH.ReportResult} report
4152 * @param {Element} container Parent element to render the report into.
4153 */
4154 renderReport(report, container) {
4155 // If any mutations happen to the report within the renderers, we want the original object untouched
4156 const clone = /** @type {LH.ReportResult} */ (JSON.parse(JSON.stringify(report)));
4157
4158 // TODO(phulce): we all agree this is technical debt we should fix
4159 if (typeof clone.categories !== 'object') throw new Error('No categories provided.');
4160 clone.reportCategories = Object.values(clone.categories);
4161 ReportRenderer.smooshAuditResultsIntoCategories(clone.audits, clone.reportCategories);
4162
4163 container.textContent = ''; // Remove previous report.
4164 container.appendChild(this._renderReport(clone));
4165 return /** @type {Element} **/ (container);
4166 }
4167
4168 /**
4169 * Define a custom element for <templates> to be extracted from. For example:
4170 * this.setTemplateContext(new DOMParser().parseFromString(htmlStr, 'text/html'))
4171 * @param {ParentNode} context
4172 */
4173 setTemplateContext(context) {
4174 this._templateContext = context;
4175 }
4176
4177 /**
4178 * @param {LH.ReportResult} report
4179 * @return {DocumentFragment}
4180 */
4181 _renderReportHeader(report) {
4182 const el = this._dom.cloneTemplate('#tmpl-lh-heading', this._templateContext);
4183 const domFragment = this._dom.cloneTemplate('#tmpl-lh-scores-wrapper', this._templateContext);
4184 const placeholder = this._dom.find('.lh-scores-wrapper-placeholder', el);
4185 /** @type {HTMLDivElement} */ (placeholder.parentNode).replaceChild(domFragment, placeholder);
4186
4187 this._dom.find('.lh-config__timestamp', el).textContent =
4188 Util.formatDateTime(report.fetchTime);
4189 this._dom.find('.lh-product-info__version', el).textContent = report.lighthouseVersion;
4190 const metadataUrl = /** @type {HTMLAnchorElement} */ (this._dom.find('.lh-metadata__url', el));
4191 const toolbarUrl = /** @type {HTMLAnchorElement}*/ (this._dom.find('.lh-toolbar__url', el));
4192 metadataUrl.href = metadataUrl.textContent = report.finalUrl;
4193 toolbarUrl.href = toolbarUrl.textContent = report.finalUrl;
4194
4195 const emulationDescriptions = Util.getEmulationDescriptions(report.configSettings || {});
4196 this._dom.find('.lh-config__emulation', el).textContent = emulationDescriptions.summary;
4197 return el;
4198 }
4199
4200 /**
4201 * @return {Element}
4202 */
4203 _renderReportShortHeader() {
4204 const shortHeaderContainer = this._dom.createElement('div', 'lh-header-container');
4205 const wrapper = this._dom.cloneTemplate('#tmpl-lh-scores-wrapper', this._templateContext);
4206 shortHeaderContainer.appendChild(wrapper);
4207 return shortHeaderContainer;
4208 }
4209
4210
4211 /**
4212 * @param {LH.ReportResult} report
4213 * @return {DocumentFragment}
4214 */
4215 _renderReportFooter(report) {
4216 const footer = this._dom.cloneTemplate('#tmpl-lh-footer', this._templateContext);
4217
4218 const env = this._dom.find('.lh-env__items', footer);
4219 env.id = 'runtime-settings';
4220 const envValues = Util.getEnvironmentDisplayValues(report.configSettings || {});
4221 [
4222 {name: 'URL', description: report.finalUrl},
4223 {name: 'Fetch time', description: Util.formatDateTime(report.fetchTime)},
4224 ...envValues,
4225 {name: 'User agent', description: report.userAgent},
4226 ].forEach(runtime => {
4227 const item = this._dom.cloneTemplate('#tmpl-lh-env__items', env);
4228 this._dom.find('.lh-env__name', item).textContent = `${runtime.name}:`;
4229 this._dom.find('.lh-env__description', item).textContent = runtime.description;
4230 env.appendChild(item);
4231 });
4232
4233 this._dom.find('.lh-footer__version', footer).textContent = report.lighthouseVersion;
4234 return footer;
4235 }
4236
4237 /**
4238 * Returns a div with a list of top-level warnings, or an empty div if no warnings.
4239 * @param {LH.ReportResult} report
4240 * @return {Node}
4241 */
4242 _renderReportWarnings(report) {
4243 if (!report.runWarnings || report.runWarnings.length === 0) {
4244 return this._dom.createElement('div');
4245 }
4246
4247 const container = this._dom.cloneTemplate('#tmpl-lh-warnings--toplevel', this._templateContext);
4248 const warnings = this._dom.find('ul', container);
4249 for (const warningString of report.runWarnings) {
4250 const warning = warnings.appendChild(this._dom.createElement('li'));
4251 warning.textContent = warningString;
4252 }
4253
4254 return container;
4255 }
4256
4257 /**
4258 * @param {LH.ReportResult} report
4259 * @return {DocumentFragment}
4260 */
4261 _renderReport(report) {
4262 let header;
4263 const headerContainer = this._dom.createElement('div');
4264 if (this._dom.isDevTools()) {
4265 headerContainer.classList.add('lh-header-plain');
4266 header = this._renderReportShortHeader();
4267 } else {
4268 headerContainer.classList.add('lh-header-sticky');
4269 header = this._renderReportHeader(report);
4270 }
4271 headerContainer.appendChild(header);
4272 const scoresContainer = this._dom.find('.lh-scores-container', headerContainer);
4273
4274 const container = this._dom.createElement('div', 'lh-container');
4275 const reportSection = container.appendChild(this._dom.createElement('div', 'lh-report'));
4276
4277 reportSection.appendChild(this._renderReportWarnings(report));
4278
4279 let scoreHeader;
4280 const isSoloCategory = report.reportCategories.length === 1;
4281 if (!isSoloCategory) {
4282 scoreHeader = this._dom.createElement('div', 'lh-scores-header');
4283 } else {
4284 headerContainer.classList.add('lh-header--solo-category');
4285 }
4286
4287 const detailsRenderer = new DetailsRenderer(this._dom);
4288 const categoryRenderer = new CategoryRenderer(this._dom, detailsRenderer);
4289 categoryRenderer.setTemplateContext(this._templateContext);
4290 const perfCategoryRenderer = new PerformanceCategoryRenderer(this._dom, detailsRenderer);
4291 perfCategoryRenderer.setTemplateContext(this._templateContext);
4292
4293 const categories = reportSection.appendChild(this._dom.createElement('div', 'lh-categories'));
4294
4295 for (const category of report.reportCategories) {
4296 if (scoreHeader) {
4297 scoreHeader.appendChild(categoryRenderer.renderScoreGauge(category));
4298 }
4299
4300 let renderer = categoryRenderer;
4301 if (category.id === 'performance') {
4302 renderer = perfCategoryRenderer;
4303 }
4304 categories.appendChild(renderer.render(category, report.categoryGroups));
4305 }
4306
4307 if (scoreHeader) {
4308 const scoreScale = this._dom.cloneTemplate('#tmpl-lh-scorescale', this._templateContext);
4309 scoresContainer.appendChild(scoreHeader);
4310 scoresContainer.appendChild(scoreScale);
4311 }
4312
4313 reportSection.appendChild(this._renderReportFooter(report));
4314
4315 const reportFragment = this._dom.createFragment();
4316 reportFragment.appendChild(headerContainer);
4317 reportFragment.appendChild(container);
4318
4319 return reportFragment;
4320 }
4321
4322 /**
4323 * Place the AuditResult into the auditDfn (which has just weight & group)
4324 * @param {Object<string, LH.Audit.Result>} audits
4325 * @param {Array<LH.ReportResult.Category>} reportCategories
4326 */
4327 static smooshAuditResultsIntoCategories(audits, reportCategories) {
4328 for (const category of reportCategories) {
4329 category.auditRefs.forEach(auditMeta => {
4330 const result = audits[auditMeta.id];
4331 auditMeta.result = result;
4332 });
4333 }
4334 }
4335}
4336
4337if (typeof module !== 'undefined' && module.exports) {
4338 module.exports = ReportRenderer;
4339} else {
4340 self.ReportRenderer = ReportRenderer;
4341}
4342
4343 //# sourceURL=compiled-reportrenderer.js
4344 </script>
4345 <script>window.__LIGHTHOUSE_JSON__ = {"userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36","lighthouseVersion":"3.0.2","fetchTime":"2018-07-17T12:57:23.508Z","requestedUrl":"https://jakegore.me/","finalUrl":"https://jakegore.me/","runWarnings":[],"audits":{"first-contentful-paint":{"id":"first-contentful-paint","title":"First Contentful Paint","description":"First contentful paint marks the time at which the first text/image is painted. [Learn more](https://developers.google.com/web/fundamentals/performance/user-centric-performance-metrics#first_paint_and_first_contentful_paint).","score":0.98,"scoreDisplayMode":"numeric","rawValue":1652.40085,"displayValue":["%10d ms",1652.40085]},"first-meaningful-paint":{"id":"first-meaningful-paint","title":"First Meaningful Paint","description":"First Meaningful Paint measures when the primary content of a page is visible. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/first-meaningful-paint).","score":0.77,"scoreDisplayMode":"numeric","rawValue":2927.5875,"displayValue":["%10d ms",2927.5875]},"speed-index":{"id":"speed-index","title":"Speed Index","description":"Speed Index shows how quickly the contents of a page are visibly populated. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/speed-index).","score":0.74,"scoreDisplayMode":"numeric","rawValue":4448.619903399027,"displayValue":["%10d ms",4448.619903399027]},"screenshot-thumbnails":{"id":"screenshot-thumbnails","title":"Screenshot Thumbnails","description":"This is what the load of your site looked like.","score":null,"scoreDisplayMode":"informative","rawValue":true,"details":{"type":"filmstrip","scale":3000,"items":[{"timing":300,"timestamp":271775303000,"data":"/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIANUAeAMBEQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD//Z"},{"timing":600,"timestamp":271775603000,"data":"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"},{"timing":900,"timestamp":271775903000,"data":"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"},{"timing":1200,"timestamp":271776203000,"data":"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"},{"timing":1500,"timestamp":271776503000,"data":"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"},{"timing":1800,"timestamp":271776803000,"data":"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dEvJYpri183dJASUO6RnGMgHo1AHVUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQB8vvrHxnuU8X3tjruo3K6b4seDT7N/D8cAu9Me+tgQnmQqzpDAblRKpPmbdwL5XK6DW+p738PdU1DWfDZudStr+1uPt99EiakqrM0KXcqQyFVjj2q8So6gruCsoYs2WNMlHS0hhQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAIelAHmXib4i+NLPxJqOneHPh7/AMJNZ2TpE98NbhtP3hjSQrsdc8CRec4NYznUv7kb/M+ty/K8qr4dVcfj/Yye0fZyndbXutN01b/M9E06f7Xawz7Nnmxh9pOSMjOK2vdHyk4qE5R3s2i1QSFABQAUAFABQAUAFABQAUAFABQAjdKaE9jmfC5H9ueLuf8AmKR/+kVrURWrOzEv3KP+H/26RraCQdIsMH/l2j/9BFUtjCtpVkn3f5mjQZhQAUAFABQAUAFABQAUAFABQAUAIRkEUAeB/F/4VeLta+JGk3/hjWPENhpF/IDrY03VxaogURorxJkfPsU5JDZwuOmK5KtOpKScHp1P0jIM1yrC5fXhmFOnKqtafNTcu902umuiutbs93tLVLO3jhj4jjUIoPoK61orH5w3dt97v7yagQUAFABQAUAFABQAUAFABQAUAFABQAmKAFoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA/wD/2Q=="},{"timing":2100,"timestamp":271777103000,"data":"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dEvJYpri183dJASUO6RnGMgHo1AHVUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQB8vvrHxnuU8X3tjruo3K6b4seDT7N/D8cAu9Me+tgQnmQqzpDAblRKpPmbdwL5XK6DW+p738PdU1DWfDZudStr+1uPt99EiakqrM0KXcqQyFVjj2q8So6gruCsoYs2WNMlHS0hhQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAIelAHmXib4i+NLPxJqOneHPh7/AMJNZ2TpE98NbhtP3hjSQrsdc8CRec4NYznUv7kb/M+ty/K8qr4dVcfj/Yye0fZyndbXutN01b/M9E06f7Xawz7Nnmxh9pOSMjOK2vdHyk4qE5R3s2i1QSFABQAUAFABQAUAFABQAUAFABQAjdKaE9jmfC5H9ueLuf8AmKR/+kVrURWrOzEv3KP+H/26RraCQdIsMH/l2j/9BFUtjCtpVkn3f5mjQZhQAUAFABQAUAFABQAUAFABQAUAIRkEUAeB/F/4VeLta+JGk3/hjWPENhpF/IDrY03VxaogURorxJkfPsU5JDZwuOmK5KtOpKScHp1P0jIM1yrC5fXhmFOnKqtafNTcu902umuiutbs93tLVLO3jhj4jjUIoPoK61orH5w3dt97v7yagQUAFABQAUAFABQAUAFABQAUAFABQAmKAFoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA/wD/2Q=="},{"timing":2400,"timestamp":271777403000,"data":"/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIANUAeAMBEQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1QMqBtpYbsZxnnHrQA4HI4oAr6lqVpo2nXWoahdQ2NhaRPPcXVzII4oY1BZndjgKoAJJPAAoAsKwYAjkHvQAtABQAUAFABQAUAFABQAUAFABQBzOr+EZ768E9vqL2ytKXmiEa4lBj8shuzYB3DcDyq9gAFY1jU5ZRlbY6UDApmRneJdMn1vw9qmnW1z9juLu1lt47ndKPKZkKh8xSRycEg/JIjcfK6nBABfiUogUnJHf1oAfQAUAFABQAUAFABQAUAFABQAUAfjnP+3/8AHlFyPHYH/cHsP/jFAH0Z8Ff2ofih4o+HWnanrHigXt9O8paY6fax5AkZQMLGBwBjpQBJ8Uf2sviH4K8L3urx+IXRLby8iLTraRjucIMKyjPJ9RQB9O/sk/EzUvjF+z94W8X6tdvfX+o/a/MuJLdIGcR3c0S/Inyj5UA49MnnNAHr9ABQAUAFABQAUAFABQAUAFABQB/PfHbTX9xDbW8Tz3E7rFFEoyzsxwqgdySQKAPr/wCACmL4UaOjY3K06nBBGRPIOooAq/H25ksvhX4guYkgldPs+FuIEmQZnQco4KnrkZHBweooA+0v+CfF1Lefsh+ApZo7eKRvt5MdrAkEYH2+5AwiAKOB2Huec0AfRNABQAUAFABQAUAFABQAUAFABQB+DOkzp8PfC7+LriRoNVufMs9GhTIfcQVluecAhASq45L56bOQD6P/AGeZmuPhFokrxiJ385mQdATM5OKAJPjtdSWHwo8RXEKWzyD7MoW7gjni+a4RfmSQFT17g4xnqBQB9nfsBX8mp/sk+BbmWO3ieT7flbWBIIxi/uBwiAKOmTgckk0AfQtABQAUAFABQAUAFABQAUAFABQB/PD4h8VXXxB8QPqMsYtNLtkW2sLBD8kECcIg9QAB9TzQB9kfs94Hwl0YA5H73H/f16AJ/jpeyab8JfElzClvJLGLXC3VvHPGc3CDlJFZTweMg4OD1GaAPtL9g67e+/ZP8B3Dx28TyR3ZZLW3SCMH7ZP0RAFH4Ac0Ae+0AFABQAUAFABQAUAFABQAUAFAH8/mhfDvWtX8HeIfEWnWXn6D4cFsNTullRRb+e5jh+UkM25gR8oOMc4FAH2J+z74Z1L/AIVRZ7bYkWUskM+HX5GLzMB154jfkZHHuMgDPj3JqXhb4Sa5fwqLeZ4rd4jIiSAr9qEbgqwI7OMEZ79waAPtH9iq+n1L9m3wxPcbPM+06lGBHGsahE1C5RAFUADCqo/CgD3GgAoAKACgAoAKACgAoAKACgAoA/EH4ZTv4V+E/wAUfAus6dfzReMbezeLUvOMP2OazaWaJcCGTf5kjImPlHqyg7lTV7Afavws+Lb2fw10bTrm0uzJBouk6X5ouirf6JCY3YccFixcA5weeuCGNmH8cfjd/wAIx8H/ABPeWOnb717e7idpm3oXvJWiEuDks6rKMl2bPlIDkCkxH0F+wNrV94h/ZO8Eajqc7XF/O+oGaR1CsxGoXI5AA54pgfQdABQAUAFABQAUAFABQAUAFABQB+f+m+A9J1DT5rmf7RF5QAby7dXXJbqMuCcLknjAxnOOKAOhtvAFjpyQRyXcqozln8qINHChK7Vdwfkfk8EEZxQBla78NPDviC3l0TXJm1KwnkhW7CxfuNvmAjLq+Qy4Lj1244waAPqf9m7wvpXgv4OaJouiWy2el2ct4kESk8D7XMe5JySSep68cUAem0AFABQAUAFABQAUAFABQAUAFAH5nr8SfCMh2p4q0Nm9F1GEn/0KgDvtM0ay1HSbq5mv0tb6IsVjlZf3gwCMAkEk4Ycf3h9CAc9PJt3dOQD/ADoA+yv2dG3fBzw+fX7R/wClMtAHpNABQAUAFABQAUAFABQAUAFABQB/N34d0W98UXb2ttcKjrGZf3rtg4IHYHnkVnUmqceZnuZNlc86xawlKSi2m7vy3P0C8K+O45PC+nvffaJb6KyhNzKFUh5MIrEcjqxz0Fc31qNm7H2lfgLFUJ0qbxEb1G1s9LQlP/223zLcfi601acRW6TKSucyBR0+hNaU66nLlSPMzvhLEZHhfrVWqpJtRsk+v/DH3T+zW+/4K+HW9ftP/pTLXSfBHp1ABQAUAFABQAUAFABQAUAFABQB/Oz8G/F8HgfxedVuPIaNbWWLZdIzxvvAUqwXkjBPHT1yOKwrJuPuq59dwvPDUMffF1PZxakua9rf1ZH3Fpmrat4ks9P1q30fT7YeVFd262cIRJCTCRkFs4xHntyzetcHJVaeh+sPMclo1KEIY3mSk7tu7S5JxVn6yRY1/V9V1fUtPlv9HsNKS2ieKP7DFsBU4O0/MeAQSB23N610Uoz505RsfM8T4jK3lboYLF+0k5xdm77Xuz7p/Zibf8D/AA2f+vn/ANKpa7j8fPU6ACgAoAKACgAoAKACgAoAKACgD+aNKAP0s+G6NF8PfDCOpV10u1VlPUERLkUAQ+M5RDJaH1Df0oA+3/2WH8z4D+GG9ftX/pVNQB6vQAUAFABQAUAFABQAUAFABQAUAfjCP2UEON+pI47/AOjEf+zUAfQ+mWv2HTbW24/cxLHwPQAUAcv8SvCt/wCJdPtv7NvmsLqGTJkUZypHI/PB/CgD7e/ZF0u70X9nrwpZ39yby7j+175iMbs3cxH6ED8KAPYaACgAoAKACgAoAKACgAoAKACgD88aACgBGG4YoA+yv2exj4QaAP8Ar4/9KJKAPRaACgAoAKACgAoAKACgAoAKACgD5c/4ZU8Sf9BXSv8AvqT/AOIoAP8AhlTxJ/0FdK/76k/+IoAP+GVPEn/QU0v/AL7k/wDiKAPefhh4VuvBHgfTdEvJYpri183dJASUO6RnGMgHo1AHVUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQB8vvrHxnuU8X3tjruo3K6b4seDT7N/D8cAu9Me+tgQnmQqzpDAblRKpPmbdwL5XK6DW+p738PdU1DWfDZudStr+1uPt99EiakqrM0KXcqQyFVjj2q8So6gruCsoYs2WNMlHS0hhQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAIelAHmXib4i+NLPxJqOneHPh7/AMJNZ2TpE98NbhtP3hjSQrsdc8CRec4NYznUv7kb/M+ty/K8qr4dVcfj/Yye0fZyndbXutN01b/M9E06f7Xawz7Nnmxh9pOSMjOK2vdHyk4qE5R3s2i1QSFABQAUAFABQAUAFABQAUAFABQAjdKaE9jmfC5H9ueLuf8AmKR/+kVrURWrOzEv3KP+H/26RraCQdIsMH/l2j/9BFUtjCtpVkn3f5mjQZhQAUAFABQAUAFABQAUAFABQAUAIRkEUAeB/F/4VeLta+JGk3/hjWPENhpF/IDrY03VxaogURorxJkfPsU5JDZwuOmK5KtOpKScHp1P0jIM1yrC5fXhmFOnKqtafNTcu902umuiutbs93tLVLO3jhj4jjUIoPoK61orH5w3dt97v7yagQUAFABQAUAFABQAUAFABQAUAFABQAmKAFoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA/wD/2Q=="},{"timing":2700,"timestamp":271777703000,"data":"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"},{"timing":3000,"timestamp":271778003000,"data":"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"}]}},"estimated-input-latency":{"id":"estimated-input-latency","title":"Estimated Input Latency","description":"The score above is an estimate of how long your app takes to respond to user input, in milliseconds, during the busiest 5s window of page load. If your latency is higher than 50 ms, users may perceive your app as laggy. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/estimated-input-latency).","score":0,"scoreDisplayMode":"numeric","rawValue":445.6,"displayValue":["%d ms",445.6]},"time-to-first-byte":{"id":"time-to-first-byte","title":"Keep server response times low (TTFB)","description":"Time To First Byte identifies the time at which your server sends a response. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/ttfb).","score":1,"scoreDisplayMode":"binary","rawValue":72,"displayValue":"","details":{"type":"opportunity","overallSavingsMs":-528,"headings":[],"items":[]}},"first-cpu-idle":{"id":"first-cpu-idle","title":"First CPU Idle","description":"First CPU Idle marks the first time at which the page's main thread is quiet enough to handle input. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/first-interactive).","score":0.34,"scoreDisplayMode":"numeric","rawValue":7871.8718499999995,"displayValue":["%10d ms",7871.8718499999995]},"interactive":{"id":"interactive","title":"Time to Interactive","description":"Interactive marks the time at which the page is fully interactive. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/consistently-interactive).","score":0.44,"scoreDisplayMode":"numeric","rawValue":7871.8718499999995,"displayValue":["%10d ms",7871.8718499999995]},"user-timings":{"id":"user-timings","title":"User Timing marks and measures","description":"Consider instrumenting your app with the User Timing API to create custom, real-world measurements of key user experiences. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/user-timing).","score":null,"scoreDisplayMode":"not-applicable","rawValue":true,"details":{"type":"table","headings":[],"items":[]}},"critical-request-chains":{"id":"critical-request-chains","title":"Critical Request Chains","description":"The Critical Request Chains below show you what resources are issued with a high priority. Consider reducing the length of chains, reducing the download size of resources, or deferring the download of unnecessary resources to improve page load. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/critical-request-chains).","score":null,"scoreDisplayMode":"informative","rawValue":false,"displayValue":"9 chains found","details":{"type":"criticalrequestchain","header":{"type":"text","text":"View critical network waterfall:"},"chains":{"5D454BF4D268740D817C2B227FA911A9":{"request":{"url":"https://jakegore.me/","startTime":271775.005,"endTime":271775.093,"responseReceivedTime":271775.08,"transferSize":1789},"children":{"4808.2016":{"request":{"url":"https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600,700","startTime":271775.245999,"endTime":271775.428,"responseReceivedTime":271775.289,"transferSize":751},"children":{}},"4808.2017":{"request":{"url":"https://jakegore.me/css/style.js","startTime":271775.2555,"endTime":271775.425,"responseReceivedTime":271775.28650000005,"transferSize":29982},"children":{}},"4808.2021":{"request":{"url":"https://jakegore.me/js/type.js","startTime":271775.4535,"endTime":271775.5,"responseReceivedTime":271775.47749900003,"transferSize":12259},"children":{}},"4808.2022":{"request":{"url":"https://www.googletagmanager.com/gtag/js?id=UA-117417362-1","startTime":271775.458999,"endTime":271775.511,"responseReceivedTime":271775.485999,"transferSize":25289},"children":{}},"4808.2023":{"request":{"url":"https://jakegore.me/js/polyfill.js","startTime":271775.517999,"endTime":271775.568,"responseReceivedTime":271775.54299999995,"transferSize":90575},"children":{}},"4808.2024":{"request":{"url":"https://jakegore.me/js/bundle.js","startTime":271775.5675,"endTime":271775.752,"responseReceivedTime":271775.67849900003,"transferSize":2197},"children":{}},"4808.2034":{"request":{"url":"https://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7lujVj9w.woff2","startTime":271775.619499,"endTime":271775.785,"responseReceivedTime":271775.7025,"transferSize":13026},"children":{}},"4808.2041":{"request":{"url":"https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlxdu3cOWxw.woff2","startTime":271775.634,"endTime":271775.812,"responseReceivedTime":271775.717,"transferSize":12982},"children":{}},"4808.2048":{"request":{"url":"https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu3cOWxw.woff2","startTime":271775.639,"endTime":271775.819,"responseReceivedTime":271775.719999,"transferSize":12666},"children":{}}}},"4808.2104":{"request":{"url":"https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2","startTime":271777.284,"endTime":271777.303,"responseReceivedTime":271777.3,"transferSize":10995},"children":{}}},"longestChain":{"duration":2298.000000009779,"length":1,"transferSize":10995}}},"redirects":{"id":"redirects","title":"Avoid multiple page redirects","description":"Redirects introduce additional delays before the page can be loaded. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/redirects).","score":1,"scoreDisplayMode":"numeric","rawValue":0,"displayValue":["%d ms",0],"details":{"type":"table","headings":[],"items":[],"summary":{"wastedMs":0}}},"mainthread-work-breakdown":{"id":"mainthread-work-breakdown","title":"Has significant main thread work","description":"Consider reducing the time spent parsing, compiling and executing JS. You may find delivering smaller JS payloads helps with this.","score":0.08,"scoreDisplayMode":"numeric","rawValue":8496,"displayValue":["%10d ms",8496],"details":{"type":"table","headings":[{"key":"groupLabel","itemType":"text","text":"Category"},{"key":"duration","itemType":"ms","granularity":1,"text":"Time Spent"}],"items":[{"group":"scriptEvaluation","groupLabel":"Script Evaluation","duration":4512},{"group":"other","groupLabel":"Other","duration":1428},{"group":"scriptParseCompile","groupLabel":"Script Parsing & Compilation","duration":784},{"group":"styleLayout","groupLabel":"Style & Layout","duration":552},{"group":"parseHTML","groupLabel":"Parse HTML & CSS","duration":512},{"group":"garbageCollection","groupLabel":"Garbage Collection","duration":400},{"group":"paintCompositeRender","groupLabel":"Rendering","duration":308}]}},"bootup-time":{"id":"bootup-time","title":"JavaScript boot-up time is too high","description":"Consider reducing the time spent parsing, compiling, and executing JS. You may find delivering smaller JS payloads helps with this. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/bootup).","score":0.3,"scoreDisplayMode":"numeric","rawValue":5244,"displayValue":["%10d ms",5244],"details":{"type":"table","headings":[{"key":"url","itemType":"url","text":"URL"},{"key":"total","granularity":1,"itemType":"ms","text":"Total"},{"key":"scripting","granularity":1,"itemType":"ms","text":"Script Evaluation"},{"key":"scriptParseCompile","granularity":1,"itemType":"ms","text":"Script Parsing & Compilation"}],"items":[{"url":"https://maps.googleapis.com/maps-api-v3/api/js/33/8/intl/en_gb/common.js","total":716,"scripting":548,"scriptParseCompile":8},{"url":"https://maps.googleapis.com/maps/api/js?key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&callback=initMap","total":592,"scripting":364,"scriptParseCompile":68},{"url":"https://jakegore.me/js/polyfill.js","total":380,"scripting":260,"scriptParseCompile":80},{"url":"chrome-extension://jabopobgcpjmedljpbcaablpmlmfcogm/jquery.min.js","total":328,"scripting":140,"scriptParseCompile":128},{"url":"chrome-extension://gighmmpiobklfepjocnamgkkbiglidom/polyfill.js","total":312,"scripting":268,"scriptParseCompile":4},{"url":"extensions::messaging","total":284,"scripting":264,"scriptParseCompile":0},{"url":"https://maps.googleapis.com/maps-api-v3/api/js/33/8/intl/en_gb/util.js","total":244,"scripting":120,"scriptParseCompile":124},{"url":"https://www.google-analytics.com/analytics.js","total":216,"scripting":192,"scriptParseCompile":4},{"url":"https://maps.googleapis.com/maps-api-v3/api/js/33/8/intl/en_gb/map.js","total":204,"scripting":152,"scriptParseCompile":40},{"url":"chrome-extension://gppongmhjkpfnbhagpmjfkannfbllamg/js/iframe.js","total":184,"scripting":164,"scriptParseCompile":20},{"url":"https://maps.googleapis.com/maps-api-v3/api/js/33/8/intl/en_gb/controls.js","total":176,"scripting":152,"scriptParseCompile":4},{"url":"chrome-extension://dbepggeogbaibhgnhhndojpepiihcmeb/lib/keyboard_utils.js","total":164,"scripting":160,"scriptParseCompile":4},{"url":"chrome-extension://gighmmpiobklfepjocnamgkkbiglidom/include.preload.js","total":160,"scripting":144,"scriptParseCompile":16},{"url":"https://jakegore.me/","total":156,"scripting":156,"scriptParseCompile":0},{"url":"chrome-extension://dbepggeogbaibhgnhhndojpepiihcmeb/lib/settings.js","total":136,"scripting":124,"scriptParseCompile":4},{"url":"chrome-extension://fmkadmapgofadopljbjfkapdkoienihi/build/inject.js","total":112,"scripting":100,"scriptParseCompile":12},{"url":"https://maps.googleapis.com/maps/api/js/ViewportInfoService.GetViewportInfo?1m6&1m2&1d49.34369421768409&2d-10.229790264641792&2m2&1d55.561802025603626&2d3.610514211050031&2u7&4sen-GB&5e0&6sm%40427000000&7b0&8e0&callback=_xdc_._p6gq8i&key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&token=59638","total":112,"scripting":92,"scriptParseCompile":12},{"url":"chrome-extension://gppongmhjkpfnbhagpmjfkannfbllamg/js/content.js","total":104,"scripting":92,"scriptParseCompile":0},{"url":"https://www.googletagmanager.com/gtag/js?id=UA-117417362-1","total":92,"scripting":88,"scriptParseCompile":4},{"url":"https://maps.googleapis.com/maps-api-v3/api/js/33/8/intl/en_gb/onion.js","total":92,"scripting":92,"scriptParseCompile":0},{"url":"https://jakegore.me/js/type.js","total":80,"scripting":60,"scriptParseCompile":20},{"url":"https://maps.googleapis.com/maps-api-v3/api/js/33/8/intl/en_gb/marker.js","total":80,"scripting":48,"scriptParseCompile":32},{"url":"chrome-extension://gppongmhjkpfnbhagpmjfkannfbllamg/js/inject.js","total":72,"scripting":64,"scriptParseCompile":0},{"url":"chrome-extension://gighmmpiobklfepjocnamgkkbiglidom/adblock-bandaids.js","total":72,"scripting":48,"scriptParseCompile":24},{"url":"extensions::sendRequest","total":64,"scripting":64,"scriptParseCompile":0},{"url":"chrome-extension://jabopobgcpjmedljpbcaablpmlmfcogm/wf_chrome.js","total":60,"scripting":52,"scriptParseCompile":8},{"url":"chrome-extension://nlkobfbkblfhlcobdomlhmpbbhmcbkfd/content.js","total":52,"scripting":48,"scriptParseCompile":4}],"summary":{"wastedMs":5244}}},"uses-rel-preload":{"id":"uses-rel-preload","title":"Preload key requests","description":"Consider using \u003clink rel=preload> to prioritize fetching late-discovered resources sooner. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/preload).","score":1,"scoreDisplayMode":"numeric","rawValue":0,"displayValue":["Potential savings of %10d ms",0],"details":{"type":"opportunity","headings":[],"items":[],"overallSavingsMs":0}},"uses-rel-preconnect":{"id":"uses-rel-preconnect","title":"Avoid multiple, costly round trips to any origin","description":"Consider adding preconnect or dns-prefetch resource hints to establish early connections to important third-party origins. [Learn more](https://developers.google.com/web/fundamentals/performance/resource-prioritization#preconnect).","score":1,"scoreDisplayMode":"numeric","rawValue":0,"displayValue":["Potential savings of %10d ms",0],"details":{"type":"opportunity","headings":[],"items":[],"overallSavingsMs":0}},"font-display":{"id":"font-display","title":"Text is invisible while webfonts are loading","description":"Leverage the font-display CSS feature to ensure text is user-visible while webfonts are loading. [Learn more](https://developers.google.com/web/updates/2016/02/font-display).","score":0,"scoreDisplayMode":"binary","rawValue":false,"details":{"type":"table","headings":[{"key":"url","itemType":"url","text":"Font URL"},{"key":"wastedTime","itemType":"ms","granularity":1,"text":"Font download time"}],"items":[{"url":"https://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7lujVj9w.woff2","wastedTime":165.5009999522008},{"url":"https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlxdu3cOWxw.woff2","wastedTime":177.99999995622784},{"url":"https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu3cOWxw.woff2","wastedTime":179.99999999301508},{"url":"https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2","wastedTime":19.00000002933666}]}},"network-requests":{"id":"network-requests","title":"Network Requests","description":"Lists the network requests that were made during page load.","score":null,"scoreDisplayMode":"informative","rawValue":42,"details":{"type":"table","headings":[{"key":"url","itemType":"url","text":"URL"},{"key":"startTime","itemType":"ms","granularity":1,"text":"Start Time"},{"key":"endTime","itemType":"ms","granularity":1,"text":"End Time"},{"key":"transferSize","itemType":"bytes","displayUnit":"kb","granularity":1,"text":"Transfer Size"},{"key":"statusCode","itemType":"text","text":"Status Code"},{"key":"mimeType","itemType":"text","text":"MIME Type"},{"key":"resourceType","itemType":"text","text":"Resource Type"}],"items":[{"url":"https://jakegore.me/","startTime":0,"endTime":87.99999998882413,"transferSize":1789,"statusCode":200,"mimeType":"text/html","resourceType":"Document"},{"url":"https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600,700","startTime":240.99899997236207,"endTime":423.0000000097789,"transferSize":751,"statusCode":200,"mimeType":"text/css","resourceType":"Stylesheet"},{"url":"https://jakegore.me/css/style.js","startTime":250.50000002374873,"endTime":419.99999998370185,"transferSize":29982,"statusCode":200,"mimeType":"application/javascript","resourceType":"Script"},{"url":"https://jakegore.me/img/dev.svg","startTime":234.99999998603016,"endTime":424.99999998835847,"transferSize":3419,"statusCode":200,"mimeType":"image/svg+xml","resourceType":"Image"},{"url":"https://jakegore.me/img/seo.svg","startTime":241.5000000037253,"endTime":434.0000000083819,"transferSize":729,"statusCode":200,"mimeType":"image/svg+xml","resourceType":"Image"},{"url":"https://maps.googleapis.com/maps/api/js?key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&callback=initMap","startTime":500.9999999892898,"endTime":596.0000000195578,"transferSize":27283,"statusCode":200,"mimeType":"text/javascript","resourceType":"Script"},{"url":"https://jakegore.me/js/type.js","startTime":448.499999998603,"endTime":494.9999999953434,"transferSize":12259,"statusCode":200,"mimeType":"application/javascript","resourceType":"Script"},{"url":"https://www.googletagmanager.com/gtag/js?id=UA-117417362-1","startTime":453.99900001939386,"endTime":505.9999999939464,"transferSize":25289,"statusCode":200,"mimeType":"application/javascript","resourceType":"Script"},{"url":"https://jakegore.me/js/polyfill.js","startTime":512.9989999695681,"endTime":563.0000000237487,"transferSize":90575,"statusCode":200,"mimeType":"application/javascript","resourceType":"Script"},{"url":"https://jakegore.me/js/bundle.js","startTime":562.5,"endTime":746.999999973923,"transferSize":2197,"statusCode":200,"mimeType":"application/javascript","resourceType":"Script"},{"url":"blob:https://jakegore.me/1a3074ad-8766-4e94-8c0e-c669d8ef95aa","startTime":192.99999997019768,"endTime":509.00000002002344,"transferSize":0,"statusCode":-1,"mimeType":"","resourceType":"Script"},{"url":"https://jakegore.me/img/background.jpg","startTime":619.4990000221878,"endTime":803.0000000144355,"transferSize":157081,"statusCode":200,"mimeType":"image/jpeg","resourceType":"Image"},{"url":"https://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7lujVj9w.woff2","startTime":614.4990000175312,"endTime":779.999999969732,"transferSize":13026,"statusCode":200,"mimeType":"font/woff2","resourceType":"Font"},{"url":"https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlxdu3cOWxw.woff2","startTime":629.0000000153668,"endTime":806.9999999715947,"transferSize":12982,"statusCode":200,"mimeType":"font/woff2","resourceType":"Font"},{"url":"https://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu3cOWxw.woff2","startTime":634.0000000200234,"endTime":814.0000000130385,"transferSize":12666,"statusCode":200,"mimeType":"font/woff2","resourceType":"Font"},{"url":"https://www.google-analytics.com/analytics.js","startTime":697.5000000093132,"endTime":820.9999999962747,"transferSize":14450,"statusCode":200,"mimeType":"text/javascript","resourceType":"Script"},{"url":"https://maps.googleapis.com/maps-api-v3/api/js/33/8/intl/en_gb/common.js","startTime":935.9999999869615,"endTime":1023.9999999757856,"transferSize":30439,"statusCode":200,"mimeType":"text/javascript","resourceType":"Script"},{"url":"https://maps.googleapis.com/maps-api-v3/api/js/33/8/intl/en_gb/util.js","startTime":934.0000000083819,"endTime":1025.000000023283,"transferSize":52241,"statusCode":200,"mimeType":"text/javascript","resourceType":"Script"},{"url":"https://maps.googleapis.com/maps-api-v3/api/js/33/8/intl/en_gb/map.js","startTime":962.5000000232831,"endTime":1033.9999999850988,"transferSize":17981,"statusCode":200,"mimeType":"text/javascript","resourceType":"Script"},{"url":"https://maps.googleapis.com/maps-api-v3/api/js/33/8/intl/en_gb/marker.js","startTime":963.5000000125729,"endTime":1036.000000021886,"transferSize":11913,"statusCode":200,"mimeType":"text/javascript","resourceType":"Script"},{"url":"https://www.google-analytics.com/r/collect?v=1&_v=j68&a=581431893&t=pageview&_s=1&dl=https%3A%2F%2Fjakegore.me%2F&ul=en-gb&de=UTF-8&dt=Jake%20Gore%20%7C%20Web%20Developer&sd=24-bit&sr=412x732&vp=412x732&je=0&_u=AACAAUAB~&jid=98007008&gjid=128775079&cid=938294087.1529431282&tid=UA-117417362-1&_gid=1702929069.1531760087&_r=1>m=u6t&z=1552098094","startTime":1017.5000000162981,"endTime":1072.9999999748543,"transferSize":113,"statusCode":200,"mimeType":"image/gif","resourceType":"Image"},{"url":"blob:https://jakegore.me/c5fcbe64-9a6d-40bf-8fb6-afcd2f6da656","startTime":1174.9999999883585,"endTime":1477.0000000135042,"transferSize":0,"statusCode":200,"mimeType":"text/plain","resourceType":"Script"},{"url":"https://maps.gstatic.com/mapfiles/openhand_8_8.cur","startTime":1408.9999999850988,"endTime":1646.999999997206,"transferSize":416,"statusCode":200,"mimeType":"image/bmp","resourceType":"Image"},{"url":"https://maps.googleapis.com/maps-api-v3/api/js/33/8/intl/en_gb/onion.js","startTime":1415.9990000189282,"endTime":1614.000000001397,"transferSize":11651,"statusCode":200,"mimeType":"text/javascript","resourceType":"Script"},{"url":"chrome-extension://gppongmhjkpfnbhagpmjfkannfbllamg/js/inject.js","startTime":1403.9999999804422,"endTime":1669.9999999837019,"transferSize":0,"statusCode":200,"mimeType":"application/javascript","resourceType":"Script"},{"url":"blob:https://jakegore.me/15018070-1e56-411d-b61a-d5c343649603","startTime":1550.9999999776483,"endTime":1685.9999999869615,"transferSize":0,"statusCode":200,"mimeType":"text/plain","resourceType":"Script"},{"url":"https://maps.googleapis.com/maps/api/js/ViewportInfoService.GetViewportInfo?1m6&1m2&1d49.34369421768409&2d-10.229790264641792&2m2&1d55.561802025603626&2d3.610514211050031&2u7&4sen-GB&5e0&6sm%40427000000&7b0&8e0&callback=_xdc_._p6gq8i&key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&token=59638","startTime":1657.4990000226535,"endTime":1718.9999999827705,"transferSize":5119,"statusCode":200,"mimeType":"text/javascript","resourceType":"Script"},{"url":"https://maps.googleapis.com/maps/vt?pb=!1m5!1m4!1i7!2i63!3i42!4i256!2m3!1e0!2sm!3i427131684!3m14!2sen-GB!3sUS!5e18!12m1!1e68!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2zcy50OjF8cC5zOi0xMDAscy50OjE4fHAudjpvZmYscy50OjV8cC5zOi0xMDB8cC5sOjY1fHAudjpvbixzLnQ6MnxwLnM6LTEwMHxwLmw6NTB8cC52OnNpbXBsaWZpZWQscy50OjN8cC5zOi0xMDAscy50OjQ5fHAudjpzaW1wbGlmaWVkLHMudDo1MHxwLmw6MzAscy50OjUxfHAubDo0MCxzLnQ6NHxwLnM6LTEwMHxwLnY6c2ltcGxpZmllZCxzLnQ6NnxzLmU6Z3xwLmg6I2ZmZmYwMHxwLmw6LTV8cC5zOi05NyxzLnQ6NnxzLmU6bHxwLmw6LTI1fHAuczotMTAw!4e0!5m1!5f2!23i1301875&key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&token=26898","startTime":1801.9990000175312,"endTime":1841.0000000149012,"transferSize":28976,"statusCode":200,"mimeType":"image/png","resourceType":"Image"},{"url":"https://maps.googleapis.com/maps/vt?pb=!1m5!1m4!1i7!2i62!3i42!4i256!2m3!1e0!2sm!3i427131684!3m14!2sen-GB!3sUS!5e18!12m1!1e68!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2zcy50OjF8cC5zOi0xMDAscy50OjE4fHAudjpvZmYscy50OjV8cC5zOi0xMDB8cC5sOjY1fHAudjpvbixzLnQ6MnxwLnM6LTEwMHxwLmw6NTB8cC52OnNpbXBsaWZpZWQscy50OjN8cC5zOi0xMDAscy50OjQ5fHAudjpzaW1wbGlmaWVkLHMudDo1MHxwLmw6MzAscy50OjUxfHAubDo0MCxzLnQ6NHxwLnM6LTEwMHxwLnY6c2ltcGxpZmllZCxzLnQ6NnxzLmU6Z3xwLmg6I2ZmZmYwMHxwLmw6LTV8cC5zOi05NyxzLnQ6NnxzLmU6bHxwLmw6LTI1fHAuczotMTAw!4e0!5m1!5f2!23i1301875&key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&token=112242","startTime":1797.000000020489,"endTime":1842.9999999934807,"transferSize":11873,"statusCode":200,"mimeType":"image/png","resourceType":"Image"},{"url":"https://maps.googleapis.com/maps/vt?pb=!1m5!1m4!1i7!2i62!3i41!4i256!2m3!1e0!2sm!3i427131684!3m14!2sen-GB!3sUS!5e18!12m1!1e68!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2zcy50OjF8cC5zOi0xMDAscy50OjE4fHAudjpvZmYscy50OjV8cC5zOi0xMDB8cC5sOjY1fHAudjpvbixzLnQ6MnxwLnM6LTEwMHxwLmw6NTB8cC52OnNpbXBsaWZpZWQscy50OjN8cC5zOi0xMDAscy50OjQ5fHAudjpzaW1wbGlmaWVkLHMudDo1MHxwLmw6MzAscy50OjUxfHAubDo0MCxzLnQ6NHxwLnM6LTEwMHxwLnY6c2ltcGxpZmllZCxzLnQ6NnxzLmU6Z3xwLmg6I2ZmZmYwMHxwLmw6LTV8cC5zOi05NyxzLnQ6NnxzLmU6bHxwLmw6LTI1fHAuczotMTAw!4e0!5m1!5f2!23i1301875&key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&token=1341","startTime":1798.000000009779,"endTime":1847.0000000088476,"transferSize":13083,"statusCode":200,"mimeType":"image/png","resourceType":"Image"},{"url":"https://maps.googleapis.com/maps/vt?pb=!1m5!1m4!1i7!2i63!3i41!4i256!2m3!1e0!2sm!3i427131684!3m14!2sen-GB!3sUS!5e18!12m1!1e68!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2zcy50OjF8cC5zOi0xMDAscy50OjE4fHAudjpvZmYscy50OjV8cC5zOi0xMDB8cC5sOjY1fHAudjpvbixzLnQ6MnxwLnM6LTEwMHxwLmw6NTB8cC52OnNpbXBsaWZpZWQscy50OjN8cC5zOi0xMDAscy50OjQ5fHAudjpzaW1wbGlmaWVkLHMudDo1MHxwLmw6MzAscy50OjUxfHAubDo0MCxzLnQ6NHxwLnM6LTEwMHxwLnY6c2ltcGxpZmllZCxzLnQ6NnxzLmU6Z3xwLmg6I2ZmZmYwMHxwLmw6LTV8cC5zOi05NyxzLnQ6NnxzLmU6bHxwLmw6LTI1fHAuczotMTAw!4e0!5m1!5f2!23i1301875&key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&token=47068","startTime":1798.4989999677055,"endTime":1862.0000000228174,"transferSize":25320,"statusCode":200,"mimeType":"image/png","resourceType":"Image"},{"url":"https://maps.gstatic.com/mapfiles/transparent.png","startTime":1847.4989999667741,"endTime":1915.999999968335,"transferSize":287,"statusCode":200,"mimeType":"image/png","resourceType":"Image"},{"url":"https://maps.googleapis.com/maps/vt?pb=!1m4!1m3!1i7!2i62!3i41!1m4!1m3!1i7!2i63!3i41!1m4!1m3!1i7!2i62!3i42!1m4!1m3!1i7!2i63!3i42!2m3!1e0!2sm!3i427131684!3m14!2sen-GB!3sUS!5e18!12m1!1e68!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2zcy50OjF8cC5zOi0xMDAscy50OjE4fHAudjpvZmYscy50OjV8cC5zOi0xMDB8cC5sOjY1fHAudjpvbixzLnQ6MnxwLnM6LTEwMHxwLmw6NTB8cC52OnNpbXBsaWZpZWQscy50OjN8cC5zOi0xMDAscy50OjQ5fHAudjpzaW1wbGlmaWVkLHMudDo1MHxwLmw6MzAscy50OjUxfHAubDo0MCxzLnQ6NHxwLnM6LTEwMHxwLnY6c2ltcGxpZmllZCxzLnQ6NnxzLmU6Z3xwLmg6I2ZmZmYwMHxwLmw6LTV8cC5zOi05NyxzLnQ6NnxzLmU6bHxwLmw6LTI1fHAuczotMTAw!4e3!12m1!5b1!23i1301875&callback=_xdc_._17xskt&key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&token=125747","startTime":1878.499999991618,"endTime":1903.9999999804422,"transferSize":426,"statusCode":200,"mimeType":"text/javascript","resourceType":"Script"},{"url":"https://maps.gstatic.com/mapfiles/api-3/images/spotlight-poi2_hdpi.png","startTime":1887.998999969568,"endTime":1949.0000000223517,"transferSize":1686,"statusCode":200,"mimeType":"image/png","resourceType":"Image"},{"url":"https://maps.googleapis.com/maps-api-v3/api/js/33/8/intl/en_gb/controls.js","startTime":2162.000000011176,"endTime":2179.0000000037253,"transferSize":25058,"statusCode":200,"mimeType":"text/javascript","resourceType":"Script"},{"url":"https://maps.googleapis.com/maps/api/js/AuthenticationService.Authenticate?1shttps%3A%2F%2Fjakegore.me%2F&4sAIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&callback=_xdc_._ke621g&key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&token=28122","startTime":2180.500000016764,"endTime":2233.0000000074506,"transferSize":85,"statusCode":200,"mimeType":"text/javascript","resourceType":"Script"},{"url":"https://fonts.googleapis.com/css?family=Roboto:300,400,500,700","startTime":2226.4999999897555,"endTime":2266.9999999925494,"transferSize":986,"statusCode":200,"mimeType":"text/css","resourceType":"Stylesheet"},{"url":"https://maps.gstatic.com/mapfiles/api-3/images/google_white5_hdpi.png","startTime":2244.9999999953434,"endTime":2366.9999999692664,"transferSize":3425,"statusCode":200,"mimeType":"image/png","resourceType":"Image"},{"url":"https://maps.gstatic.com/mapfiles/api-3/images/mapcnt6.png","startTime":2245.999999984633,"endTime":2368.000000016764,"transferSize":2285,"statusCode":200,"mimeType":"image/png","resourceType":"Image"},{"url":"https://maps.gstatic.com/mapfiles/api-3/images/sv9.png","startTime":2246.9989999663085,"endTime":2369.0000000060536,"transferSize":9457,"statusCode":200,"mimeType":"image/png","resourceType":"Image"},{"url":"https://maps.googleapis.com/maps/api/js/QuotaService.RecordEvent?1shttps%3A%2F%2Fjakegore.me%2F&3sAIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&7sppda0d&10e1&callback=_xdc_._9wubkq&key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&token=40885","startTime":2268.999000021722,"endTime":2374.00000001071,"transferSize":86,"statusCode":200,"mimeType":"text/javascript","resourceType":"Script"},{"url":"https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2","startTime":2278.999999980442,"endTime":2298.000000009779,"transferSize":10995,"statusCode":200,"mimeType":"font/woff2","resourceType":"Font"}]}},"metrics":{"id":"metrics","title":"Metrics","description":"Collects all available metrics.","score":null,"scoreDisplayMode":"informative","rawValue":7871.8718499999995,"details":{"items":[{"firstContentfulPaint":1652,"firstMeaningfulPaint":2928,"firstCPUIdle":7872,"interactive":7872,"speedIndex":4449,"estimatedInputLatency":446,"observedNavigationStart":0,"observedNavigationStartTs":271775003000,"observedFirstPaint":572,"observedFirstPaintTs":271775575000,"observedFirstContentfulPaint":572,"observedFirstContentfulPaintTs":271775575000,"observedFirstMeaningfulPaint":1069,"observedFirstMeaningfulPaintTs":271776072000,"observedTraceEnd":3589,"observedTraceEndTs":271778592000,"observedLoad":1976,"observedLoadTs":271776979000,"observedDomContentLoaded":758,"observedDomContentLoadedTs":271775761000,"observedFirstVisualChange":445,"observedFirstVisualChangeTs":271775448000,"observedLastVisualChange":2694,"observedLastVisualChangeTs":271777697000,"observedSpeedIndex":1198,"observedSpeedIndexTs":271776200730}]}},"uses-long-cache-ttl":{"id":"uses-long-cache-ttl","title":"Uses efficient cache policy on static assets","description":"A long cache lifetime can speed up repeat visits to your page. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/cache-policy).","score":0.76,"scoreDisplayMode":"numeric","rawValue":57665.93125,"displayValue":"7 assets found","details":{"type":"table","headings":[{"key":"url","itemType":"url","text":"URL"},{"key":"cacheLifetimeMs","itemType":"ms","text":"Cache TTL","displayUnit":"duration"},{"key":"totalBytes","itemType":"bytes","text":"Size (KB)","displayUnit":"kb","granularity":1}],"items":[{"url":"blob:https://jakegore.me/c5fcbe64-9a6d-40bf-8fb6-afcd2f6da656","cacheControl":null,"cacheLifetimeMs":0,"cacheHitProbability":0,"totalBytes":0,"wastedBytes":0},{"url":"blob:https://jakegore.me/15018070-1e56-411d-b61a-d5c343649603","cacheControl":null,"cacheLifetimeMs":0,"cacheHitProbability":0,"totalBytes":0,"wastedBytes":0},{"url":"https://www.googletagmanager.com/gtag/js?id=UA-117417362-1","cacheControl":{"private":true,"max-age":900},"cacheLifetimeMs":900000,"cacheHitProbability":0.10625000000000001,"totalBytes":25289,"wastedBytes":22602.04375},{"url":"https://maps.googleapis.com/maps/api/js?key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&callback=initMap","cacheControl":{"public":true,"max-age":1800},"cacheLifetimeMs":1800000,"cacheHitProbability":0.1375,"totalBytes":27283,"wastedBytes":23531.5875},{"url":"https://www.google-analytics.com/analytics.js","cacheControl":{"public":true,"max-age":7200},"cacheLifetimeMs":7200000,"cacheHitProbability":0.25,"totalBytes":14450,"wastedBytes":10837.5},{"url":"https://fonts.googleapis.com/css?family=Roboto:300,400,500,700","cacheControl":{"private":true,"max-age":86400,"stale-while-revalidate":"604800"},"cacheLifetimeMs":86400000,"cacheHitProbability":0.6,"totalBytes":986,"wastedBytes":394.40000000000003},{"url":"https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600,700","cacheControl":{"private":true,"max-age":86400,"stale-while-revalidate":"604800"},"cacheLifetimeMs":86400000,"cacheHitProbability":0.6,"totalBytes":751,"wastedBytes":300.40000000000003}],"summary":{"wastedBytes":57665.93125}}},"total-byte-weight":{"id":"total-byte-weight","title":"Avoids enormous network payloads","description":"Large network payloads cost users real money and are highly correlated with long load times. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/network-payloads).","score":1,"scoreDisplayMode":"numeric","rawValue":668379,"displayValue":["Total size was %d KB",652.7138671875],"details":{"type":"table","headings":[{"key":"url","itemType":"url","text":"URL"},{"key":"totalBytes","itemType":"bytes","displayUnit":"kb","granularity":1,"text":"Total Size"},{"key":"totalMs","itemType":"ms","text":"Transfer Time"}],"items":[{"url":"https://jakegore.me/img/background.jpg","totalBytes":157081,"totalMs":203.8794007472996},{"url":"https://jakegore.me/js/polyfill.js","totalBytes":90575,"totalMs":117.5595821435225},{"url":"https://maps.googleapis.com/maps-api-v3/api/js/33/8/intl/en_gb/util.js","totalBytes":52241,"totalMs":67.8049144991417},{"url":"https://maps.googleapis.com/maps-api-v3/api/js/33/8/intl/en_gb/common.js","totalBytes":30439,"totalMs":39.50754756684164},{"url":"https://jakegore.me/css/style.js","totalBytes":29982,"totalMs":38.914395714348245},{"url":"https://maps.googleapis.com/maps/vt?pb=!1m5!1m4!1i7!2i63!3i42!4i256!2m3!1e0!2sm!3i427131684!3m14!2sen-GB!3sUS!5e18!12m1!1e68!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2zcy50OjF8cC5zOi0xMDAscy50OjE4fHAudjpvZmYscy50OjV8cC5zOi0xMDB8cC5sOjY1fHAudjpvbixzLnQ6MnxwLnM6LTEwMHxwLmw6NTB8cC52OnNpbXBsaWZpZWQscy50OjN8cC5zOi0xMDAscy50OjQ5fHAudjpzaW1wbGlmaWVkLHMudDo1MHxwLmw6MzAscy50OjUxfHAubDo0MCxzLnQ6NHxwLnM6LTEwMHxwLnY6c2ltcGxpZmllZCxzLnQ6NnxzLmU6Z3xwLmg6I2ZmZmYwMHxwLmw6LTV8cC5zOi05NyxzLnQ6NnxzLmU6bHxwLmw6LTI1fHAuczotMTAw!4e0!5m1!5f2!23i1301875&key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&token=26898","totalBytes":28976,"totalMs":37.60868288369537},{"url":"https://maps.googleapis.com/maps/api/js?key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&callback=initMap","totalBytes":27283,"totalMs":35.411295386383934},{"url":"https://maps.googleapis.com/maps/vt?pb=!1m5!1m4!1i7!2i63!3i41!4i256!2m3!1e0!2sm!3i427131684!3m14!2sen-GB!3sUS!5e18!12m1!1e68!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2zcy50OjF8cC5zOi0xMDAscy50OjE4fHAudjpvZmYscy50OjV8cC5zOi0xMDB8cC5sOjY1fHAudjpvbixzLnQ6MnxwLnM6LTEwMHxwLmw6NTB8cC52OnNpbXBsaWZpZWQscy50OjN8cC5zOi0xMDAscy50OjQ5fHAudjpzaW1wbGlmaWVkLHMudDo1MHxwLmw6MzAscy50OjUxfHAubDo0MCxzLnQ6NHxwLnM6LTEwMHxwLnY6c2ltcGxpZmllZCxzLnQ6NnxzLmU6Z3xwLmg6I2ZmZmYwMHxwLmw6LTV8cC5zOi05NyxzLnQ6NnxzLmU6bHxwLmw6LTI1fHAuczotMTAw!4e0!5m1!5f2!23i1301875&key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&token=47068","totalBytes":25320,"totalMs":32.863468063748165},{"url":"https://www.googletagmanager.com/gtag/js?id=UA-117417362-1","totalBytes":25289,"totalMs":32.82323238009981},{"url":"https://maps.googleapis.com/maps-api-v3/api/js/33/8/intl/en_gb/controls.js","totalBytes":25058,"totalMs":32.52341164065567}]}},"offscreen-images":{"id":"offscreen-images","title":"Defer offscreen images","description":"Consider lazy-loading offscreen and hidden images after all critical resources have finished loading to lower time to interactive. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/offscreen-images).","score":1,"scoreDisplayMode":"numeric","rawValue":0,"displayValue":["Potential savings of %d KB",92],"warnings":[],"details":{"type":"opportunity","headings":[{"key":"url","valueType":"thumbnail","label":""},{"key":"url","valueType":"url","label":"URL"},{"key":"totalBytes","valueType":"bytes","label":"Original"},{"key":"wastedBytes","valueType":"bytes","label":"Potential Savings"}],"items":[{"url":"https://maps.googleapis.com/maps/vt?pb=!1m5!1m4!1i7!2i63!3i42!4i256!2m3!1e0!2sm!3i427131684!3m14!2sen-GB!3sUS!5e18!12m1!1e68!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2zcy50OjF8cC5zOi0xMDAscy50OjE4fHAudjpvZmYscy50OjV8cC5zOi0xMDB8cC5sOjY1fHAudjpvbixzLnQ6MnxwLnM6LTEwMHxwLmw6NTB8cC52OnNpbXBsaWZpZWQscy50OjN8cC5zOi0xMDAscy50OjQ5fHAudjpzaW1wbGlmaWVkLHMudDo1MHxwLmw6MzAscy50OjUxfHAubDo0MCxzLnQ6NHxwLnM6LTEwMHxwLnY6c2ltcGxpZmllZCxzLnQ6NnxzLmU6Z3xwLmg6I2ZmZmYwMHxwLmw6LTV8cC5zOi05NyxzLnQ6NnxzLmU6bHxwLmw6LTI1fHAuczotMTAw!4e0!5m1!5f2!23i1301875&key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&token=26898","requestStartTime":271776.806999,"totalBytes":28779,"wastedBytes":28779,"wastedPercent":100},{"url":"https://maps.googleapis.com/maps/vt?pb=!1m5!1m4!1i7!2i63!3i41!4i256!2m3!1e0!2sm!3i427131684!3m14!2sen-GB!3sUS!5e18!12m1!1e68!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2zcy50OjF8cC5zOi0xMDAscy50OjE4fHAudjpvZmYscy50OjV8cC5zOi0xMDB8cC5sOjY1fHAudjpvbixzLnQ6MnxwLnM6LTEwMHxwLmw6NTB8cC52OnNpbXBsaWZpZWQscy50OjN8cC5zOi0xMDAscy50OjQ5fHAudjpzaW1wbGlmaWVkLHMudDo1MHxwLmw6MzAscy50OjUxfHAubDo0MCxzLnQ6NHxwLnM6LTEwMHxwLnY6c2ltcGxpZmllZCxzLnQ6NnxzLmU6Z3xwLmg6I2ZmZmYwMHxwLmw6LTV8cC5zOi05NyxzLnQ6NnxzLmU6bHxwLmw6LTI1fHAuczotMTAw!4e0!5m1!5f2!23i1301875&key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&token=47068","requestStartTime":271776.803499,"totalBytes":25276,"wastedBytes":25276,"wastedPercent":100},{"url":"https://maps.googleapis.com/maps/vt?pb=!1m5!1m4!1i7!2i62!3i41!4i256!2m3!1e0!2sm!3i427131684!3m14!2sen-GB!3sUS!5e18!12m1!1e68!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2zcy50OjF8cC5zOi0xMDAscy50OjE4fHAudjpvZmYscy50OjV8cC5zOi0xMDB8cC5sOjY1fHAudjpvbixzLnQ6MnxwLnM6LTEwMHxwLmw6NTB8cC52OnNpbXBsaWZpZWQscy50OjN8cC5zOi0xMDAscy50OjQ5fHAudjpzaW1wbGlmaWVkLHMudDo1MHxwLmw6MzAscy50OjUxfHAubDo0MCxzLnQ6NHxwLnM6LTEwMHxwLnY6c2ltcGxpZmllZCxzLnQ6NnxzLmU6Z3xwLmg6I2ZmZmYwMHxwLmw6LTV8cC5zOi05NyxzLnQ6NnxzLmU6bHxwLmw6LTI1fHAuczotMTAw!4e0!5m1!5f2!23i1301875&key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&token=1341","requestStartTime":271776.803,"totalBytes":13039,"wastedBytes":13039,"wastedPercent":100},{"url":"https://maps.googleapis.com/maps/vt?pb=!1m5!1m4!1i7!2i62!3i42!4i256!2m3!1e0!2sm!3i427131684!3m14!2sen-GB!3sUS!5e18!12m1!1e68!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2zcy50OjF8cC5zOi0xMDAscy50OjE4fHAudjpvZmYscy50OjV8cC5zOi0xMDB8cC5sOjY1fHAudjpvbixzLnQ6MnxwLnM6LTEwMHxwLmw6NTB8cC52OnNpbXBsaWZpZWQscy50OjN8cC5zOi0xMDAscy50OjQ5fHAudjpzaW1wbGlmaWVkLHMudDo1MHxwLmw6MzAscy50OjUxfHAubDo0MCxzLnQ6NHxwLnM6LTEwMHxwLnY6c2ltcGxpZmllZCxzLnQ6NnxzLmU6Z3xwLmg6I2ZmZmYwMHxwLmw6LTV8cC5zOi05NyxzLnQ6NnxzLmU6bHxwLmw6LTI1fHAuczotMTAw!4e0!5m1!5f2!23i1301875&key=AIzaSyDC_YNvutGm6mAnhuKqp4VKDXraaZalEQo&token=112242","requestStartTime":271776.802,"totalBytes":11829,"wastedBytes":11829,"wastedPercent":100},{"url":"https://maps.gstatic.com/mapfiles/api-3/images/sv9.png","requestStartTime":271777.251999,"totalBytes":9431,"wastedBytes":9431,"wastedPercent":100},{"url":"https://maps.gstatic.com/mapfiles/api-3/images/google_white5_hdpi.png","requestStartTime":271777.25,"totalBytes":3399,"wastedBytes":3399,"wastedPercent":100},{"url":"https://maps.gstatic.com/mapfiles/api-3/images/mapcnt6.png","requestStartTime":271777.251,"totalBytes":2259,"wastedBytes":2259,"wastedPercent":100}],"overallSavingsMs":0,"overallSavingsBytes":94012}},"render-blocking-resources":{"id":"render-blocking-resources","title":"Eliminate render-blocking resources","description":"Resources are blocking the first paint of your page. Consider delivering critical JS/CSS inline and deferring all non-critical JS/styles. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/blocking-resources).","score":0.65,"scoreDisplayMode":"numeric","rawValue":482,"displayValue":"2 resources delayed first paint by 482ms","details":{"type":"opportunity","headings":[{"key":"url","valueType":"url","label":"URL"},{"key":"totalBytes","valueType":"bytes","label":"Size (KB)"},{"key":"wastedMs","valueType":"timespanMs","label":"Download Time (ms)"}],"items":[{"url":"https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600,700","totalBytes":751,"wastedMs":482},{"url":"https://jakegore.me/css/style.js","totalBytes":29982,"wastedMs":300}],"overallSavingsMs":482}},"unminified-css":{"id":"unminified-css","title":"Minify CSS","description":"Minifying CSS files can reduce network payload sizes. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/minify-css).","score":1,"scoreDisplayMode":"numeric","rawValue":0,"displayValue":["Potential savings of %d KB",2],"details":{"type":"opportunity","headings":[{"key":"url","valueType":"url","label":"URL"},{"key":"totalBytes","valueType":"bytes","label":"Original"},{"key":"wastedBytes","valueType":"bytes","label":"Potential Savings"}],"items":[{"url":"body {\n margin: 0;\n padding: 0;\n font-family: \"Source Sans Pro\", sans-serif; ... } ...","totalBytes":11353,"wastedBytes":2540,"wastedPercent":22.36853695058575}],"overallSavingsMs":0,"overallSavingsBytes":2540}},"unminified-javascript":{"id":"unminified-javascript","title":"Minify JavaScript","description":"Minifying JavaScript files can reduce payload sizes and script parse time. [Learn more](https://developers.google.com/speed/docs/insights/MinifyResources).","score":1,"scoreDisplayMode":"numeric","rawValue":0,"displayValue":"","warnings":[],"details":{"type":"opportunity","headings":[],"items":[],"overallSavingsMs":0,"overallSavingsBytes":0}},"unused-css-rules":{"id":"unused-css-rules","title":"Defer unused CSS","description":"Remove unused rules from stylesheets to reduce unnecessary bytes consumed by network activity. [Learn more](https://developers.google.com/speed/docs/insights/OptimizeCSSDelivery).","score":1,"scoreDisplayMode":"numeric","rawValue":0,"displayValue":["Potential savings of %d KB",9],"details":{"type":"opportunity","headings":[{"key":"url","valueType":"url","label":"URL"},{"key":"totalBytes","valueType":"bytes","label":"Original"},{"key":"wastedBytes","valueType":"bytes","label":"Potential Savings"}],"items":[{"url":"body {\n margin: 0;\n padding: 0;\n font-family: \"Source Sans Pro\", sans-serif; ... } ...","wastedBytes":8810,"wastedPercent":77.59623007134678,"totalBytes":11353}],"overallSavingsMs":0,"overallSavingsBytes":8810}},"uses-webp-images":{"id":"uses-webp-images","title":"Serve images in next-gen formats","description":"Image formats like JPEG 2000, JPEG XR, and WebP often provide better compression than PNG or JPEG, which means faster downloads and less data consumption. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/webp).","score":1,"scoreDisplayMode":"numeric","rawValue":0,"displayValue":["Potential savings of %d KB",94],"warnings":[],"details":{"type":"opportunity","headings":[{"key":"url","valueType":"thumbnail","label":""},{"key":"url","valueType":"url","label":"URL"},{"key":"totalBytes","valueType":"bytes","label":"Original"},{"key":"wastedBytes","valueType":"bytes","label":"Potential Savings"}],"items":[{"url":"https://jakegore.me/img/background.jpg","fromProtocol":true,"isCrossOrigin":false,"totalBytes":156688,"wastedBytes":96030}],"overallSavingsMs":0,"overallSavingsBytes":96030}},"uses-optimized-images":{"id":"uses-optimized-images","title":"Efficiently encode images","description":"Optimized images load faster and consume less cellular data. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/optimize-images).","score":1,"scoreDisplayMode":"numeric","rawValue":0,"displayValue":"","warnings":[],"details":{"type":"opportunity","headings":[],"items":[],"overallSavingsMs":0,"overallSavingsBytes":0}},"uses-text-compression":{"id":"uses-text-compression","title":"Enable text compression","description":"Text-based responses should be served with compression (gzip, deflate or brotli) to minimize total network bytes. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/text-compression).","score":0.84,"scoreDisplayMode":"numeric","rawValue":190,"displayValue":["Potential savings of %d KB",91],"details":{"type":"opportunity","headings":[{"key":"url","valueType":"url","label":"Uncompressed resource URL"},{"key":"totalBytes","valueType":"bytes","label":"Original"},{"key":"wastedBytes","valueType":"bytes","label":"GZIP Savings"}],"items":[{"url":"https://jakegore.me/js/polyfill.js","totalBytes":90244,"wastedBytes":60954},{"url":"https://jakegore.me/css/style.js","totalBytes":29723,"wastedBytes":23960},{"url":"https://jakegore.me/js/type.js","totalBytes":12027,"wastedBytes":8514}],"overallSavingsMs":190,"overallSavingsBytes":93428}},"uses-responsive-images":{"id":"uses-responsive-images","title":"Properly size images","description":"Serve images that are appropriately-sized to save cellular data and improve load time. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/oversized-images).","score":1,"scoreDisplayMode":"numeric","rawValue":0,"displayValue":["Potential savings of %d KB",72],"warnings":[],"details":{"type":"opportunity","headings":[{"key":"url","valueType":"thumbnail","label":""},{"key":"url","valueType":"url","label":"URL"},{"key":"totalBytes","valueType":"bytes","label":"Original"},{"key":"wastedBytes","valueType":"bytes","label":"Potential Savings"}],"items":[{"url":"https://jakegore.me/img/background.jpg","totalBytes":156688,"wastedBytes":73615,"wastedPercent":46.98199389940238}],"overallSavingsMs":0,"overallSavingsBytes":73615}},"efficient-animated-content":{"id":"efficient-animated-content","title":"Use video formats for animated content","description":"Large GIFs are inefficient for delivering animated content. Consider using MPEG4/WebM videos for animations and PNG/WebP for static images instead of GIF to save network bytes. [Learn more](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/replace-animated-gifs-with-video/)","score":1,"scoreDisplayMode":"numeric","rawValue":0,"displayValue":"","details":{"type":"opportunity","headings":[],"items":[],"overallSavingsMs":0,"overallSavingsBytes":0}},"dom-size":{"id":"dom-size","title":"Avoids an excessive DOM size","description":"Browser engineers recommend pages contain fewer than ~1,500 DOM nodes. The sweet spot is a tree depth \u003c 32 elements and fewer than 60 children/parent element. A large DOM can increase memory usage, cause longer [style calculations](https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations), and produce costly [layout reflows](https://developers.google.com/speed/articles/reflow). [Learn more](https://developers.google.com/web/tools/lighthouse/audits/dom-size).","score":1,"scoreDisplayMode":"numeric","rawValue":185,"displayValue":["%d nodes",185],"details":{"type":"table","headings":[{"key":"totalNodes","itemType":"text","text":"Total DOM Nodes"},{"key":"depth","itemType":"text","text":"Maximum DOM Depth"},{"key":"width","itemType":"text","text":"Maximum Children"}],"items":[{"totalNodes":"185","depth":"14","width":"24"},{"totalNodes":"","depth":{"type":"code","value":"\u003cdiv style=\"width: 256px; height: 256px;\">"},"width":{"type":"code","value":"\u003chead>"}}]}}},"configSettings":{"output":"html","maxWaitForLoad":45000,"throttlingMethod":"simulate","throttling":{"rttMs":150,"throughputKbps":1638.4,"requestLatencyMs":562.5,"downloadThroughputKbps":1474.5600000000002,"uploadThroughputKbps":675,"cpuSlowdownMultiplier":4},"auditMode":false,"gatherMode":false,"disableStorageReset":false,"disableDeviceEmulation":false,"blockedUrlPatterns":null,"additionalTraceCategories":null,"extraHeaders":null,"onlyAudits":null,"onlyCategories":["performance"],"skipAudits":null},"categories":{"performance":{"title":"Performance","auditRefs":[{"id":"first-contentful-paint","weight":3,"group":"metrics"},{"id":"first-meaningful-paint","weight":1,"group":"metrics"},{"id":"speed-index","weight":4,"group":"metrics"},{"id":"interactive","weight":5,"group":"metrics"},{"id":"first-cpu-idle","weight":2,"group":"metrics"},{"id":"estimated-input-latency","weight":0,"group":"metrics"},{"id":"render-blocking-resources","weight":0,"group":"load-opportunities"},{"id":"uses-responsive-images","weight":0,"group":"load-opportunities"},{"id":"offscreen-images","weight":0,"group":"load-opportunities"},{"id":"unminified-css","weight":0,"group":"load-opportunities"},{"id":"unminified-javascript","weight":0,"group":"load-opportunities"},{"id":"unused-css-rules","weight":0,"group":"load-opportunities"},{"id":"uses-optimized-images","weight":0,"group":"load-opportunities"},{"id":"uses-webp-images","weight":0,"group":"load-opportunities"},{"id":"uses-text-compression","weight":0,"group":"load-opportunities"},{"id":"uses-rel-preconnect","weight":0,"group":"load-opportunities"},{"id":"time-to-first-byte","weight":0,"group":"load-opportunities"},{"id":"redirects","weight":0,"group":"load-opportunities"},{"id":"uses-rel-preload","weight":0,"group":"load-opportunities"},{"id":"efficient-animated-content","weight":0,"group":"load-opportunities"},{"id":"total-byte-weight","weight":0,"group":"diagnostics"},{"id":"uses-long-cache-ttl","weight":0,"group":"diagnostics"},{"id":"dom-size","weight":0,"group":"diagnostics"},{"id":"critical-request-chains","weight":0,"group":"diagnostics"},{"id":"network-requests","weight":0},{"id":"metrics","weight":0},{"id":"user-timings","weight":0,"group":"diagnostics"},{"id":"bootup-time","weight":0,"group":"diagnostics"},{"id":"screenshot-thumbnails","weight":0},{"id":"mainthread-work-breakdown","weight":0,"group":"diagnostics"},{"id":"font-display","weight":0,"group":"diagnostics"}],"id":"performance","score":0.64}},"categoryGroups":{"metrics":{"title":"Metrics"},"load-opportunities":{"title":"Opportunities","description":"These are opportunities to speed up your application by optimizing the following resources."},"diagnostics":{"title":"Diagnostics","description":"More information about the performance of your application."},"a11y-color-contrast":{"title":"Color Contrast Is Satisfactory","description":"These are opportunities to improve the legibility of your content."},"a11y-describe-contents":{"title":"Elements Describe Contents Well","description":"These are opportunities to make your content easier to understand for a user of assistive technology, like a screen reader."},"a11y-well-structured":{"title":"Elements Are Well Structured","description":"These are opportunities to make sure your HTML is appropriately structured."},"a11y-aria":{"title":"ARIA Attributes Follow Best Practices","description":"These are opportunities to improve the usage of ARIA in your application which may enhance the experience for users of assistive technology, like a screen reader."},"a11y-correct-attributes":{"title":"Elements Use Attributes Correctly","description":"These are opportunities to improve the configuration of your HTML elements."},"a11y-element-names":{"title":"Elements Have Discernible Names","description":"These are opportunities to improve the semantics of the controls in your application. This may enhance the experience for users of assistive technology, like a screen reader."},"a11y-language":{"title":"Page Specifies Valid Language","description":"These are opportunities to improve the interpretation of your content by users in different locales."},"a11y-meta":{"title":"Meta Tags Used Properly","description":"These are opportunities to improve the user experience of your site."},"seo-mobile":{"title":"Mobile Friendly","description":"Make sure your pages are mobile friendly so users don’t have to pinch or zoom in order to read the content pages. [Learn more](https://developers.google.com/search/mobile-sites/)."},"seo-content":{"title":"Content Best Practices","description":"Format your HTML in a way that enables crawlers to better understand your app’s content."},"seo-crawl":{"title":"Crawling and Indexing","description":"To appear in search results, crawlers need access to your app."}},"timing":{"total":12873}};</script>
4346 <script>
4347 window.addEventListener('DOMContentLoaded', _ => {
4348 const dom = new DOM(document);
4349 const renderer = new ReportRenderer(dom);
4350
4351 const container = document.querySelector('main');
4352 renderer.renderReport(window.__LIGHTHOUSE_JSON__, container);
4353
4354 // Hook in JS features and page-level event listeners after the report
4355 // is in the document.
4356 const features = new ReportUIFeatures(dom);
4357 features.initFeatures(window.__LIGHTHOUSE_JSON__);
4358 });
4359
4360 document.addEventListener('lh-analytics', e => {
4361 if (window.ga) {
4362 ga(e.detail.cmd, e.detail.fields);
4363 }
4364 });
4365
4366 document.addEventListener('lh-log', e => {
4367 const logger = new Logger(document.querySelector('#lh-log'));
4368
4369 switch (e.detail.cmd) {
4370 case 'log':
4371 logger.log(e.detail.msg);
4372 break;
4373 case 'warn':
4374 logger.warn(e.detail.msg);
4375 break;
4376 case 'error':
4377 logger.error(e.detail.msg);
4378 break;
4379 case 'hide':
4380 logger.hide();
4381 break;
4382 }
4383 });
4384 </script>
4385</body>
4386</html>