congo/exampleSite/content/docs/version-2/lighthouse.html

11700 lines
927 KiB
HTML
Raw Permalink Normal View History

<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEhklEQVR4AWJxL/BhIAesev1U5tcflpncgNrKIsqNIwzC9feMpDUzs70kOczMzMzJJcxwCTMzncPMnOwtzBwzMzPb0vRfeZPp0VhPS5I39V5fdiXV1/VD+9QC7OVn9BsyH1XIoEI1PfmJvLFowVV564+34DFUHudbmfDh4kVXh//7XwE+WjS/YfXZe3yr4j2rqj1AIhSB7hZ8ZtPZu/zw8cK523U4wE1/rvPfWrz4zs0m9ZdC9yUJAlASdBAgocRegfF/f3/h/PuaFsxMdwjAR0vm1+06eMMfIrhLqTWqdH4EumU2SPfMhigJAlRQbZrgrRsl9U+Y2DYDFCz3ILC9kiAiqSrMwbWT0nceEnR+9Kggc2zjOJCASDENkg0a5HfZZgDP81CM3CrQs2Z1+o7DJ6ePr8sK0AOCHv5Jjdt3evyYSaZ351VIStIxPRAUtrBYbxC6w+BZ0ivVSBKkIhJhemSyZpfB00EiPO2VjzYkxhcqXQqCWCShGplvi3y0QxqbuBurMjyJeWnkHZuAEgIQGsUBqwrfjZ+IlBgKyRJzVVYF8O6qFWdh86YzQzMrZigYmxAyfvHgLZQ/LC1CbeniW2Hkqr/PH16SgvGuf2/uzNMBwJA/njxizGPtSyAf7EziJCMGRDRdhoAC4PL1A/SrKQMAAQkEfpJAcRQdrBJ7gNwjSpJsdwK+CANBkqa1LgQB4IicV9nYUct7gaxuDJUErQIiEAiMxLVOFlKzIktPpT0ggpdpC/8YAHnxbgkUY4tAAFSR7AAXNyAAWHJrA/kHGjzg5nleuwFO7Nd/IoDw4Pm58+4jNLmYG0wRA5bErc2Mr3Y+dXTDW1VvwqbJkzMCHQ4S1GTCBOIgUHJrGdEwqzR+jAp/o2qAZelUDoQnruEEdDclJI6576AlNVfc+22XN/+Y1vnJD0Yind6UpEEvn/Hqq15EYjCW7jZCJEpnNvDgkyelDjs106kuux2AAXCSobULOWP8mLhYlpoDMK4qAFXJGk+grtH8YXVz5KJblqaG1+VUdTc0I290bmUQAriGITRbdQnom0aoFj8kx1+wMD2ifncAXUQE4SkDqN1hE0jEophs1SUwZAOhUAiMCLwRtamtTZtbbmZErSAUHbSysaoEmnrsakiMiUAURi283gN6wans9oX8rOCrj7/JP35DFD+iQ7Au/K2KE1jzx6ujjUnXFH9KjEq6ZlhsTBICrNLJf47Pv/pkHzvup1w4dmUbEei0+bcXRqJuh5kVARQ8byyYxOwNGr7A87xh1tp8sGT+uMInrwi++Xj7TQz2d27NvwEkrOflAFQGIDA5khASBCGdO2/Z/MnLPwYfv5TFhjW7QhVKAB6afwe2LpFlFsCnlQEosgQgDsdOG1/LKeNqJS4JCSPJ/i+TakwEARor7gER1Iva5JmPOJK0RUqmoPnnlzFCtmIAhAAQEIQRgDaiYPIauNXcnDlRIrWNFY3hm7PG9YRqr7IV7HrCgAC17befjEvRq2nGhAHtBqDpOuI/I1diUUAMYIxEdyejBJqLnNoszGZtfiX/CztGv2mq+sdaAAAAAElFTkSuQmCC">
<title>Lighthouse Report</title>
<style>/**
* @license
* Copyright 2017 The Lighthouse Authors. All Rights Reserved.
*
* 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
*
* 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.
*/
/*
Naming convention:
If a variable is used for a specific component: --{component}-{property name}-{modifier}
Both {component} and {property name} should be kebab-case. If the target is the entire page,
use 'report' for the component. The property name should not be abbreviated. Use the
property name the variable is intended for - if it's used for multiple, a common descriptor
is fine (ex: 'size' for a variable applied to 'width' and 'height'). If a variable is shared
across multiple components, either create more variables or just drop the "{component}-"
part of the name. Append any modifiers at the end (ex: 'big', 'dark').
For colors: --color-{hue}-{intensity}
{intensity} is the Material Design tag - 700, A700, etc.
*/
.lh-vars {
/* Palette using Material Design Colors
* https://www.materialui.co/colors */
--color-amber-50: #FFF8E1;
--color-blue-200: #90CAF9;
--color-blue-900: #0D47A1;
--color-blue-A700: #2962FF;
--color-cyan-500: #00BCD4;
--color-gray-100: #F5F5F5;
--color-gray-300: #CFCFCF;
--color-gray-200: #E0E0E0;
--color-gray-400: #BDBDBD;
--color-gray-50: #FAFAFA;
--color-gray-500: #9E9E9E;
--color-gray-600: #757575;
--color-gray-700: #616161;
--color-gray-800: #424242;
--color-gray-900: #212121;
--color-gray: #000000;
--color-green-700: #018642;
--color-green: #0CCE6B;
--color-lime-400: #D3E156;
--color-orange-50: #FFF3E0;
--color-orange-700: #D04900;
--color-orange: #FFA400;
--color-red-700: #EB0F00;
--color-red: #FF4E42;
--color-teal-600: #00897B;
--color-white: #FFFFFF;
/* Context-specific colors */
--color-average-secondary: var(--color-orange-700);
--color-average: var(--color-orange);
--color-fail-secondary: var(--color-red-700);
--color-fail: var(--color-red);
--color-hover: var(--color-gray-50);
--color-informative: var(--color-blue-900);
--color-pass-secondary: var(--color-green-700);
--color-pass: var(--color-green);
--color-not-applicable: var(--color-gray-600);
/* Component variables */
--audit-description-padding-left: calc(var(--score-icon-size) + var(--score-icon-margin-left) + var(--score-icon-margin-right));
--audit-explanation-line-height: 16px;
--audit-group-margin-bottom: 40px;
--audit-group-padding-vertical: 8px;
--audit-margin-horizontal: 5px;
--audit-padding-vertical: 8px;
--category-header-font-size: 20px;
--category-padding: 40px;
--chevron-line-stroke: var(--color-gray-600);
--chevron-size: 12px;
--default-padding: 12px;
--env-item-background-color: var(--color-gray-100);
--env-item-font-size: 28px;
--env-item-line-height: 36px;
--env-item-padding: 10px 0px;
--env-name-min-width: 220px;
--footer-padding-vertical: 16px;
--gauge-circle-size-big: 112px;
--gauge-circle-size: 80px;
--gauge-label-font-size-big: 28px;
--gauge-label-font-size: 20px;
--gauge-label-line-height-big: 36px;
--gauge-label-line-height: 26px;
--gauge-percentage-font-size-big: 38px;
--gauge-percentage-font-size: 28px;
--gauge-wrapper-width: 148px;
--header-line-height: 24px;
--highlighter-background-color: var(--report-text-color);
--icon-square-size: calc(var(--score-icon-size) * 0.88);
--image-preview-size: 48px;
--metric-toggle-lines-fill: #7F7F7F;
--metrics-toggle-background-color: var(--color-gray-200);
--screenshot-overlay-background: rgba(0, 0, 0, 0.3);
--plugin-badge-background-color: var(--color-white);
--plugin-badge-size-big: calc(var(--gauge-circle-size-big) / 2.7);
--plugin-badge-size: calc(var(--gauge-circle-size) / 2.7);
--plugin-icon-size: 65%;
--pwa-icon-margin: 0 6px 0 -2px;
--pwa-icon-size: var(--topbar-logo-size);
--report-background-color: #fff;
--report-border-color-secondary: #ebebeb;
--report-font-family-monospace: 'Roboto Mono', 'Menlo', 'dejavu sans mono', 'Consolas', 'Lucida Console', monospace;
--report-font-family: Roboto, Helvetica, Arial, sans-serif;
--report-font-size: 16px;
--report-icon-size: var(--score-icon-background-size);
--report-line-height: 24px;
--report-min-width: 400px;
--report-monospace-font-size: calc(var(--report-font-size) * 0.85);
--report-text-color-secondary: var(--color-gray-800);
--report-text-color: var(--color-gray-900);
--report-width: calc(60 * var(--report-font-size));
--score-container-padding: 8px;
--score-icon-background-size: 24px;
--score-icon-margin-left: 4px;
--score-icon-margin-right: 12px;
--score-icon-margin: 0 var(--score-icon-margin-right) 0 var(--score-icon-margin-left);
--score-icon-size: 12px;
--scores-container-padding: 20px 0 20px 0;
--scorescale-height: 6px;
--scorescale-width: 18px;
--section-padding-vertical: 12px;
--snippet-background-color: var(--color-gray-50);
--snippet-color: #0938C2;
--sparkline-height: 5px;
--stackpack-padding-horizontal: 10px;
--sticky-header-background-color: var(--report-background-color);
--table-higlight-background-color: hsla(0, 0%, 75%, 0.1);
--tools-icon-color: var(--color-gray-600);
--topbar-background-color: var(--color-gray-100);
--topbar-height: 32px;
--topbar-logo-size: 24px;
--topbar-padding: 0 8px;
--toplevel-warning-background-color: var(--color-orange-50);
--toplevel-warning-message-text-color: #BD4200;
--toplevel-warning-padding: 18px;
--toplevel-warning-text-color: var(--report-text-color);
/* SVGs */
--plugin-icon-url-dark: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="%23FFFFFF"><path d="M0 0h24v24H0z" fill="none"/><path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>');
--plugin-icon-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="%23757575"><path d="M0 0h24v24H0z" fill="none"/><path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>');
--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="%23178239" 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>');
--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="%23E67700" 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>');
--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="%23C7221F" d="M2 42h44L24 4 2 42zm24-6h-4v-4h4v4zm0-8h-4v-8h4v8z"/></svg>');
--pwa-installable-gray-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="nonzero"><circle fill="%23DAE0E3" cx="12" cy="12" r="12"/><path d="M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm3.5 7.7h-2.8v2.8h-1.4v-2.8H8.5v-1.4h2.8V8.5h1.4v2.8h2.8v1.4z" fill="%23FFF"/></g></svg>');
--pwa-optimized-gray-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="%23DAE0E3" width="24" height="24" rx="12"/><path fill="%23FFF" d="M12 15.07l3.6 2.18-.95-4.1 3.18-2.76-4.2-.36L12 6.17l-1.64 3.86-4.2.36 3.2 2.76-.96 4.1z"/><path d="M5 5h14v14H5z"/></g></svg>');
--pwa-installable-gray-url-dark: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="nonzero"><circle fill="%23424242" cx="12" cy="12" r="12"/><path d="M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm3.5 7.7h-2.8v2.8h-1.4v-2.8H8.5v-1.4h2.8V8.5h1.4v2.8h2.8v1.4z" fill="%23FFF"/></g></svg>');
--pwa-optimized-gray-url-dark: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="%23424242" width="24" height="24" rx="12"/><path fill="%23FFF" d="M12 15.07l3.6 2.18-.95-4.1 3.18-2.76-4.2-.36L12 6.17l-1.64 3.86-4.2.36 3.2 2.76-.96 4.1z"/><path d="M5 5h14v14H5z"/></g></svg>');
--pwa-installable-color-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><circle fill="%230CCE6B" cx="12" cy="12" r="12"/><path d="M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm3.5 7.7h-2.8v2.8h-1.4v-2.8H8.5v-1.4h2.8V8.5h1.4v2.8h2.8v1.4z" fill="%23FFF"/></g></svg>');
--pwa-optimized-color-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="%230CCE6B" width="24" height="24" rx="12"/><path d="M5 5h14v14H5z"/><path fill="%23FFF" d="M12 15.07l3.6 2.18-.95-4.1 3.18-2.76-4.2-.36L12 6.17l-1.64 3.86-4.2.36 3.2 2.76-.96 4.1z"/></g></svg>');
}
@media not print {
.lh-vars.dark {
/* Pallete */
--color-gray-200: var(--color-gray-800);
--color-gray-300: #616161;
--color-gray-400: var(--color-gray-600);
--color-gray-700: var(--color-gray-400);
--color-gray-50: #757575;
--color-gray-600: var(--color-gray-500);
--color-green-700: var(--color-green);
--color-orange-700: var(--color-orange);
--color-red-700: var(--color-red);
--color-teal-600: var(--color-cyan-500);
/* Context-specific colors */
--color-hover: rgba(0, 0, 0, 0.2);
--color-informative: var(--color-blue-200);
/* Component variables */
--env-item-background-color: var(--color-gray);
--plugin-badge-background-color: var(--color-gray-800);
--report-background-color: var(--color-gray-900);
--report-border-color-secondary: var(--color-gray-200);
--report-text-color-secondary: var(--color-gray-400);
--report-text-color: var(--color-gray-100);
--snippet-color: var(--color-cyan-500);
--topbar-background-color: var(--color-gray);
--toplevel-warning-background-color: #544B40;
--toplevel-warning-message-text-color: var(--color-orange-700);
--toplevel-warning-text-color: var(--color-gray-100);
/* SVGs */
--plugin-icon-url: var(--plugin-icon-url-dark);
--pwa-installable-gray-url: var(--pwa-installable-gray-url-dark);
--pwa-optimized-gray-url: var(--pwa-optimized-gray-url-dark);
}
}
@media only screen and (max-width: 480px) {
.lh-vars {
--audit-group-margin-bottom: 20px;
--category-padding: 24px;
--env-name-min-width: 120px;
--gauge-circle-size-big: 96px;
--gauge-circle-size: 72px;
--gauge-label-font-size-big: 22px;
--gauge-label-font-size: 14px;
--gauge-label-line-height-big: 26px;
--gauge-label-line-height: 20px;
--gauge-percentage-font-size-big: 34px;
--gauge-percentage-font-size: 26px;
--gauge-wrapper-width: 112px;
--header-padding: 16px 0 16px 0;
--image-preview-size: 24px;
--plugin-icon-size: 75%;
--pwa-icon-margin: 0 7px 0 -3px;
--report-font-size: 14px;
--report-line-height: 20px;
--score-icon-margin-left: 2px;
--score-icon-size: 10px;
--topbar-height: 28px;
--topbar-logo-size: 20px;
}
/* Not enough space to adequately show the relative savings bars. */
.lh-sparkline {
display: none;
}
}
.lh-vars.lh-devtools {
--audit-explanation-line-height: 14px;
--audit-group-margin-bottom: 20px;
--audit-group-padding-vertical: 12px;
--audit-padding-vertical: 4px;
--category-header-font-size: 16px;
--category-padding: 12px;
--default-padding: 12px;
--env-name-min-width: 120px;
--footer-padding-vertical: 8px;
--gauge-circle-size-big: 72px;
--gauge-circle-size: 64px;
--gauge-label-font-size-big: 22px;
--gauge-label-font-size: 14px;
--gauge-label-line-height-big: 26px;
--gauge-label-line-height: 20px;
--gauge-percentage-font-size-big: 34px;
--gauge-percentage-font-size: 26px;
--gauge-wrapper-width: 97px;
--header-line-height: 20px;
--header-padding: 16px 0 16px 0;
--screenshot-overlay-background: transparent;
--plugin-icon-size: 75%;
--pwa-icon-margin: 0 7px 0 -3px;
--report-font-family-monospace: 'Menlo', 'dejavu sans mono', 'Consolas', 'Lucida Console', monospace;
--report-font-family: '.SFNSDisplay-Regular', 'Helvetica Neue', 'Lucida Grande', sans-serif;
--report-font-size: 12px;
--report-line-height: 20px;
--score-icon-margin-left: 2px;
--score-icon-size: 10px;
--section-padding-vertical: 8px;
}
.lh-devtools.lh-root {
height: 100%;
}
.lh-devtools.lh-root img {
/* Override devtools default 'min-width: 0' so svg without size in a flexbox isn't collapsed. */
min-width: auto;
}
.lh-devtools .lh-container {
overflow-y: scroll;
height: calc(100% - var(--topbar-height));
}
@media print {
.lh-devtools .lh-container {
overflow: unset;
}
}
.lh-devtools .lh-sticky-header {
/* This is normally the height of the topbar, but we want it to stick to the top of our scroll container .lh-container` */
top: 0;
}
@keyframes fadeIn {
0% { opacity: 0;}
100% { opacity: 0.6;}
}
.lh-root *, .lh-root *::before, .lh-root *::after {
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
}
.lh-root {
font-family: var(--report-font-family);
font-size: var(--report-font-size);
margin: 0;
line-height: var(--report-line-height);
background: var(--report-background-color);
scroll-behavior: smooth;
color: var(--report-text-color);
}
.lh-root :focus {
outline: -webkit-focus-ring-color auto 3px;
}
.lh-root summary:focus {
outline: none;
box-shadow: 0 0 0 1px hsl(217, 89%, 61%);
}
.lh-root [hidden] {
display: none !important;
}
.lh-root pre {
margin: 0;
}
.lh-root details > summary {
cursor: pointer;
}
.lh-container {
/*
Text wrapping in the report is so much FUN!
We have a `word-break: break-word;` globally here to prevent a few common scenarios, namely
long non-breakable text (usually URLs) found in:
1. The footer
2. .lh-node (outerHTML)
3. .lh-code
With that sorted, the next challenge is appropriate column sizing and text wrapping inside our
.lh-details tables. Even more fun.
* We don't want table headers ("Potential Savings (ms)") to wrap or their column values, but
we'd be happy for the URL column to wrap if the URLs are particularly long.
* We want the narrow columns to remain narrow, providing the most column width for URL
* We don't want the table to extend past 100% width.
* Long URLs in the URL column can wrap. Util.getURLDisplayName maxes them out at 64 characters,
but they do not get any overflow:ellipsis treatment.
*/
word-break: break-word;
}
.lh-audit-group a,
.lh-category-header__description a,
.lh-audit__description a,
.lh-warnings a,
.lh-footer a,
.lh-table-column--link a {
color: var(--color-informative);
}
.lh-audit__description, .lh-audit__stackpack {
--inner-audit-padding-right: var(--stackpack-padding-horizontal);
padding-left: var(--audit-description-padding-left);
padding-right: var(--inner-audit-padding-right);
padding-top: 8px;
padding-bottom: 8px;
}
.lh-details {
font-size: var(--report-font-size);
margin-top: var(--default-padding);
margin-bottom: var(--default-padding);
margin-left: var(--audit-description-padding-left);
/* whatever the .lh-details side margins are */
width: 100%;
}
.lh-details.flex .lh-code {
max-width: 70%;
}
.lh-audit__stackpack {
display: flex;
align-items: center;
}
.lh-audit__stackpack__img {
max-width: 50px;
margin-right: var(--default-padding)
}
/* Report header */
.report-icon {
display: flex;
align-items: center;
padding: 10px 12px;
cursor: pointer;
}
.report-icon[disabled] {
opacity: 0.3;
pointer-events: none;
}
.report-icon::before {
content: "";
margin-right: 5px;
background-repeat: no-repeat;
width: var(--report-icon-size);
height: var(--report-icon-size);
opacity: 0.7;
}
.report-icon:hover::before {
opacity: 1;
}
.dark .report-icon::before {
filter: invert(1);
}
.report-icon--print::before {
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>');
}
.report-icon--copy::before {
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>');
}
.report-icon--open::before {
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>');
}
.report-icon--download::before {
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>');
}
.report-icon--dark::before {
background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 100 125"><path d="M50 23.587c-16.27 0-22.799 12.574-22.799 21.417 0 12.917 10.117 22.451 12.436 32.471h20.726c2.32-10.02 12.436-19.554 12.436-32.471 0-8.843-6.528-21.417-22.799-21.417zM39.637 87.161c0 3.001 1.18 4.181 4.181 4.181h.426l.41 1.231C45.278 94.449 46.042 95 48.019 95h3.963c1.978 0 2.74-.551 3.365-2.427l.409-1.231h.427c3.002 0 4.18-1.18 4.18-4.181V80.91H39.637v6.251zM50 18.265c1.26 0 2.072-.814 2.072-2.073v-9.12C52.072 5.813 51.26 5 50 5c-1.259 0-2.072.813-2.072 2.073v9.12c0 1.259.813 2.072 2.072 2.072zM68.313 23.727c.994.774 2.135.634 2.91-.357l5.614-7.187c.776-.992.636-2.135-.356-2.909-.992-.776-2.135-.636-2.91.357l-5.613 7.186c-.778.993-.636 2.135.355 2.91zM91.157 36.373c-.306-1.222-1.291-1.815-2.513-1.51l-8.85 2.207c-1.222.305-1.814 1.29-1.51 2.512.305 1.223 1.291 1.814 2.513 1.51l8.849-2.206c1.223-.305 1.816-1.291 1.511-2.513zM86.757 60.48l-8.331-3.709c-1.15-.512-2.225-.099-2.736 1.052-.512 1.151-.1 2.224 1.051 2.737l8.33 3.707c1.15.514 2.225.101 2.736-1.05.513-1.149.1-2.223-1.05-2.737zM28.779 23.37c.775.992 1.917 1.131 2.909.357.992-.776 1.132-1.917.357-2.91l-5.615-7.186c-.775-.992-1.917-1.132-2.909-.357s-1.131 1.917-.356 2.909l5.614 7.187zM21.715 39.583c.305-1.223-.288-2.208-1.51-2.513l-8.849-2.207c-1.222-.303-2.208.289-2.513 1.511-.303 1.222.288 2.207 1.511 2.512l8.848 2.206c1.222.304 2.208-.287 2.513-1.509zM21.575 56.771l-8.331 3.711c-1.151.511-1.563 1.586-1.05 2.735.511 1.151 1.586 1.563 2.736 1.052l8.331-3.711c1.151-.511 1.563-1.586 1.05-2.735-.512-1.15-1.585-1.562-2.736-1.052z"/></svg>');
}
.report-icon--treemap::before {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="black"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M3 5v14h19V5H3zm2 2h15v4H5V7zm0 10v-4h4v4H5zm6 0v-4h9v4h-9z"/></svg>');
}
.lh-buttons {
display: flex;
flex-wrap: wrap;
}
.lh-button {
margin: 10px;
height: 30px;
border: 1px solid var(--color-gray-600);
border-radius: 4px;
font-weight: bold;
color: rgb(26, 115, 232);
background-color: var(--report-background-color);
}
.dark .lh-button {
color: var(--color-blue-200);
}
/* Node */
.lh-node__snippet {
font-family: var(--report-font-family-monospace);
color: var(--snippet-color);
font-size: var(--report-monospace-font-size);
line-height: 20px;
}
/* Score */
.lh-audit__score-icon {
width: var(--score-icon-size);
height: var(--score-icon-size);
margin: var(--score-icon-margin);
}
.lh-audit--pass .lh-audit__display-text {
color: var(--color-pass-secondary);
}
.lh-audit--pass .lh-audit__score-icon,
.lh-scorescale-range--pass::before {
border-radius: 100%;
background: var(--color-pass);
}
.lh-audit--average .lh-audit__display-text {
color: var(--color-average-secondary);
}
.lh-audit--average .lh-audit__score-icon,
.lh-scorescale-range--average::before {
background: var(--color-average);
width: var(--icon-square-size);
height: var(--icon-square-size);
}
.lh-audit--fail .lh-audit__display-text {
color: var(--color-fail-secondary);
}
.lh-audit--fail .lh-audit__score-icon,
.lh-audit--error .lh-audit__score-icon,
.lh-scorescale-range--fail::before {
border-left: calc(var(--score-icon-size) / 2) solid transparent;
border-right: calc(var(--score-icon-size) / 2) solid transparent;
border-bottom: var(--score-icon-size) solid var(--color-fail);
}
.lh-audit--manual .lh-audit__display-text,
.lh-audit--notapplicable .lh-audit__display-text {
color: var(--color-gray-600);
}
.lh-audit--manual .lh-audit__score-icon,
.lh-audit--notapplicable .lh-audit__score-icon {
border-radius: 100%;
background: var(--color-gray-400);
}
.lh-audit--informative .lh-audit__display-text {
color: var(--color-gray-600);
}
.lh-audit--informative .lh-audit__score-icon {
border: none;
border-radius: 100%;
background: var(--color-gray-400);
}
.lh-audit__description,
.lh-audit__stackpack {
color: var(--report-text-color-secondary);
}
.lh-audit__adorn {
border: 1px solid slategray;
border-radius: 3px;
margin: 0 3px;
padding: 0 2px;
line-height: 1.1;
display: inline-block;
font-size: 90%;
}
.lh-category-header__description {
font-size: var(--report-font-size);
text-align: center;
margin: 0px auto;
max-width: 400px;
}
.lh-audit__display-text,
.lh-load-opportunity__sparkline,
.lh-chevron-container {
margin: 0 var(--audit-margin-horizontal);
}
.lh-chevron-container {
margin-right: 0;
}
.lh-audit__title-and-text {
flex: 1;
}
.lh-audit__title-and-text code {
color: var(--snippet-color);
font-size: var(--report-monospace-font-size);
}
/* Prepend display text with em dash separator. But not in Opportunities. */
.lh-audit__display-text:not(:empty):before {
content: '—';
margin-right: var(--audit-margin-horizontal);
}
.lh-audit-group.lh-audit-group--load-opportunities .lh-audit__display-text:not(:empty):before {
display: none;
}
/* Expandable Details (Audit Groups, Audits) */
.lh-audit__header {
display: flex;
align-items: center;
font-weight: 500;
padding: var(--audit-padding-vertical) 0;
}
.lh-audit--load-opportunity .lh-audit__header {
display: block;
}
.lh-metricfilter {
text-align: right;
margin-top: var(--default-padding);
}
.lh-metricfilter__radio {
position: absolute;
left: -9999px;
}
.lh-metricfilter input[type='radio']:focus-visible + label {
outline: -webkit-focus-ring-color auto 1px;
}
.lh-metricfilter__label {
border: solid 1px var(--color-gray-400);
align-items: center;
justify-content: center;
padding: 2px 5px;
width: 50%;
height: 28px;
cursor: pointer;
font-size: 90%;
}
.lh-metricfilter__label:first-of-type {
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
margin-left: 5px;
}
.lh-metricfilter__label:last-of-type {
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
.lh-metricfilter__label--active {
background: var(--color-blue-A700);
color: var(--color-white);
}
/* Give the 'All' choice a more muted display */
.lh-metricfilter__label--active[for="metric-All"] {
background-color: var(--color-blue-200) !important;
color: black !important;
}
/* If audits are filtered, hide the itemcount for Passed Audits… */
.lh-category--filtered .lh-audit-group .lh-audit-group__itemcount {
display: none;
}
.lh-audit__header:hover {
background-color: var(--color-hover);
}
/* We want to hide the browser's default arrow marker on summary elements. Admittedly, it's complicated. */
.lh-audit-group > summary,
.lh-expandable-details > summary {
/* Blink 89+ and Firefox will hide the arrow when display is changed from (new) default of `list-item` to block. https://chromestatus.com/feature/6730096436051968*/
display: block;
}
/* Safari and Blink <=88 require using the -webkit-details-marker selector */
.lh-audit-group > summary::-webkit-details-marker,
.lh-expandable-details > summary::-webkit-details-marker {
display: none;
}
/* Perf Metric */
.lh-metrics-container {
display: grid;
grid-template-rows: 1fr 1fr 1fr;
grid-auto-flow: column;
grid-column-gap: 24px;
}
.lh-metric {
border-top: 1px solid var(--report-border-color-secondary);
}
@media screen and (min-width: 640px) {
.lh-metric:nth-child(3n+3) {
border-bottom: 1px solid var(--report-border-color-secondary);
}
}
@media screen and (max-width: 640px) {
.lh-metrics-container {
display: block;
}
.lh-metric:nth-last-child(-n+1) {
border-bottom: 1px solid var(--report-border-color-secondary);
}
}
.lh-metric__innerwrap {
display: grid;
grid-template-columns: var(--audit-description-padding-left) 10fr 3fr;
align-items: center;
padding: 10px 0;
}
.lh-metric__details {
order: -1;
}
.lh-metric__title {
flex: 1;
font-weight: 500;
}
.lh-metrics__disclaimer {
color: var(--color-gray-600);
margin: var(--section-padding-vertical) 0;
}
.lh-calclink {
padding-left: calc(1ex / 3);
}
.lh-metric__description {
display: none;
grid-column-start: 2;
grid-column-end: 3;
color: var(--report-text-color-secondary);
}
.lh-metric__value {
white-space: nowrap; /* No wrapping between metric value and the icon */
font-weight: 500;
justify-self: end;
}
/* No-JS toggle switch */
/* Keep this selector sync'd w/ `magicSelector` in report-ui-features-test.js */
.lh-metrics-toggle__input:checked ~ .lh-metrics-container .lh-metric__description {
display: block;
}
.lh-metrics-toggle__input {
cursor: pointer;
opacity: 0;
position: absolute;
right: 0;
width: 74px;
height: 28px;
top: -3px;
}
.lh-metrics-toggle__label {
display: flex;
background-color: #eee;
border-radius: 20px;
overflow: hidden;
position: absolute;
right: 0;
top: -3px;
pointer-events: none;
}
.lh-metrics-toggle__input:focus + label {
outline: -webkit-focus-ring-color auto 3px;
}
.lh-metrics-toggle__icon {
display: flex;
align-items: center;
justify-content: center;
padding: 2px 5px;
width: 50%;
height: 28px;
}
.lh-metrics-toggle__input:not(:checked) + label .lh-metrics-toggle__icon--less,
.lh-metrics-toggle__input:checked + label .lh-metrics-toggle__icon--more {
background-color: var(--color-blue-A700);
--metric-toggle-lines-fill: var(--color-white);
}
.lh-metrics-toggle__lines {
fill: var(--metric-toggle-lines-fill);
}
.lh-metrics-toggle__label {
background-color: var(--metrics-toggle-background-color);
}
.lh-metrics-toggle__label .lh-metrics-toggle__icon--less {
padding-left: 8px;
}
.lh-metrics-toggle__label .lh-metrics-toggle__icon--more {
padding-right: 8px;
}
/* Pushes the metric description toggle button to the right. */
.lh-audit-group--metrics .lh-audit-group__header {
display: flex;
}
.lh-audit-group--metrics .lh-audit-group__header span.lh-audit-group__title {
flex: 1;
}
.lh-metric .lh-metric__innerwrap::before,
.lh-scorescale-range::before {
content: '';
width: var(--score-icon-size);
height: var(--score-icon-size);
display: inline-block;
margin: var(--score-icon-margin);
}
.lh-metric--pass .lh-metric__value {
color: var(--color-pass-secondary);
}
.lh-metric--pass .lh-metric__innerwrap::before {
border-radius: 100%;
background: var(--color-pass);
}
.lh-metric--average .lh-metric__value {
color: var(--color-average-secondary);
}
.lh-metric--average .lh-metric__innerwrap::before {
background: var(--color-average);
width: var(--icon-square-size);
height: var(--icon-square-size);
}
.lh-metric--fail .lh-metric__value {
color: var(--color-fail-secondary);
}
.lh-metric--fail .lh-metric__innerwrap::before,
.lh-metric--error .lh-metric__innerwrap::before {
border-left: calc(var(--score-icon-size) / 2) solid transparent;
border-right: calc(var(--score-icon-size) / 2) solid transparent;
border-bottom: var(--score-icon-size) solid var(--color-fail);
}
.lh-metric--error .lh-metric__value,
.lh-metric--error .lh-metric__description {
color: var(--color-fail-secondary);
}
/* Perf load opportunity */
.lh-load-opportunity__cols {
display: flex;
align-items: flex-start;
}
.lh-load-opportunity__header .lh-load-opportunity__col {
color: var(--color-gray-600);
display: unset;
line-height: calc(2.3 * var(--report-font-size));
}
.lh-load-opportunity__col {
display: flex;
}
.lh-load-opportunity__col--one {
flex: 5;
align-items: center;
margin-right: 2px;
}
.lh-load-opportunity__col--two {
flex: 4;
text-align: right;
}
.lh-audit--load-opportunity .lh-audit__display-text {
text-align: right;
flex: 0 0 calc(3 * var(--report-font-size));
}
/* Sparkline */
.lh-load-opportunity__sparkline {
flex: 1;
margin-top: calc((var(--report-line-height) - var(--sparkline-height)) / 2);
}
.lh-sparkline {
height: var(--sparkline-height);
width: 100%;
}
.lh-sparkline__bar {
height: 100%;
float: right;
}
.lh-audit--pass .lh-sparkline__bar {
background: var(--color-pass);
}
.lh-audit--average .lh-sparkline__bar {
background: var(--color-average);
}
.lh-audit--fail .lh-sparkline__bar {
background: var(--color-fail);
}
/* Filmstrip */
.lh-filmstrip-container {
/* smaller gap between metrics and filmstrip */
margin: -8px auto 0 auto;
}
.lh-filmstrip {
display: flex;
flex-direction: row;
justify-content: space-between;
padding-bottom: var(--default-padding);
}
.lh-filmstrip__frame {
text-align: right;
position: relative;
}
.lh-filmstrip__thumbnail {
border: 1px solid var(--report-border-color-secondary);
max-height: 100px;
max-width: 60px;
}
@media screen and (max-width: 750px) {
.lh-filmstrip {
flex-wrap: wrap;
}
.lh-filmstrip__frame {
width: 20%;
margin-bottom: 5px;
}
.lh-filmstrip__thumbnail {
display: block;
margin: auto;
}
}
/* Audit */
.lh-audit {
border-bottom: 1px solid var(--report-border-color-secondary);
}
/* Apply border-top to just the first audit. */
.lh-audit {
border-top: 1px solid var(--report-border-color-secondary);
}
.lh-audit ~ .lh-audit {
border-top: none;
}
.lh-audit--error .lh-audit__display-text {
color: var(--color-fail);
}
/* Audit Group */
.lh-audit-group {
margin-bottom: var(--audit-group-margin-bottom);
position: relative;
}
.lh-audit-group__header::before {
/* By default, groups don't get an icon */
content: none;
width: var(--pwa-icon-size);
height: var(--pwa-icon-size);
margin: var(--pwa-icon-margin);
display: inline-block;
vertical-align: middle;
}
/* Style the "over budget" columns red. */
.lh-audit-group--budgets #performance-budget tbody tr td:nth-child(4),
.lh-audit-group--budgets #performance-budget tbody tr td:nth-child(5),
.lh-audit-group--budgets #timing-budget tbody tr td:nth-child(3) {
color: var(--color-red-700);
}
/* Align the "over budget request count" text to be close to the "over budget bytes" column. */
.lh-audit-group--budgets .lh-table tbody tr td:nth-child(4){
text-align: right;
}
.lh-audit-group--budgets .lh-table {
width: 100%;
margin: 16px 0px 16px 0px;
}
.lh-audit-group--pwa-installable .lh-audit-group__header::before {
content: '';
background-image: var(--pwa-installable-gray-url);
}
.lh-audit-group--pwa-optimized .lh-audit-group__header::before {
content: '';
background-image: var(--pwa-optimized-gray-url);
}
.lh-audit-group--pwa-installable.lh-badged .lh-audit-group__header::before {
background-image: var(--pwa-installable-color-url);
}
.lh-audit-group--pwa-optimized.lh-badged .lh-audit-group__header::before {
background-image: var(--pwa-optimized-color-url);
}
.lh-audit-group--metrics .lh-audit-group__summary {
margin-top: 0;
margin-bottom: 0;
}
.lh-audit-group__summary {
display: flex;
justify-content: space-between;
margin-top: calc(var(--category-padding) * 1.5);
margin-bottom: var(--category-padding);
}
.lh-audit-group__itemcount {
color: var(--color-gray-600);
font-weight: bold;
}
.lh-audit-group__header .lh-chevron {
margin-top: calc((var(--report-line-height) - 5px) / 2);
}
.lh-audit-group__header {
font-size: var(--report-font-size);
margin: 0 0 var(--audit-group-padding-vertical);
/* When the header takes 100% width, the chevron becomes small. */
max-width: calc(100% - var(--chevron-size));
}
/* max-width makes the metric toggle not flush. metrics doesn't have a chevron so unset. */
.lh-audit-group--metrics .lh-audit-group__header {
max-width: unset;
}
.lh-audit-group__header span.lh-audit-group__title {
font-weight: bold;
}
.lh-audit-group__header span.lh-audit-group__itemcount {
font-weight: bold;
color: var(--color-gray-600);
}
.lh-audit-group__header span.lh-audit-group__description {
font-weight: 500;
color: var(--color-gray-600);
}
.lh-audit-group__header span.lh-audit-group__description::before {
content: '—';
margin: 0px var(--audit-margin-horizontal);
}
.lh-clump > .lh-audit-group__header,
.lh-audit-group--diagnostics .lh-audit-group__header,
.lh-audit-group--load-opportunities .lh-audit-group__header,
.lh-audit-group--metrics .lh-audit-group__header,
.lh-audit-group--pwa-installable .lh-audit-group__header,
.lh-audit-group--pwa-optimized .lh-audit-group__header {
margin-top: var(--audit-group-padding-vertical);
}
.lh-audit-explanation {
margin: var(--audit-padding-vertical) 0 calc(var(--audit-padding-vertical) / 2) var(--audit-margin-horizontal);
line-height: var(--audit-explanation-line-height);
display: inline-block;
}
.lh-audit--fail .lh-audit-explanation {
color: var(--color-fail);
}
/* Report */
.lh-list > div:not(:last-child) {
padding-bottom: 20px;
}
.lh-header-container {
display: block;
margin: 0 auto;
position: relative;
word-wrap: break-word;
}
.lh-report {
min-width: var(--report-min-width);
}
.lh-exception {
font-size: large;
}
.lh-code {
white-space: normal;
margin-top: 0;
font-size: var(--report-monospace-font-size);
}
.lh-warnings {
--item-margin: calc(var(--report-line-height) / 6);
color: var(--color-average);
margin: var(--audit-padding-vertical) 0;
padding: calc(var(--audit-padding-vertical) / 2) calc(var(--audit-description-padding-left));
}
.lh-warnings span {
font-weight: bold;
}
.lh-warnings--toplevel {
--item-margin: calc(var(--header-line-height) / 4);
color: var(--toplevel-warning-text-color);
margin-left: auto;
margin-right: auto;
max-width: calc(var(--report-width) - var(--category-padding) * 2);
background-color: var(--toplevel-warning-background-color);
padding: var(--toplevel-warning-padding);
border-radius: 8px;
}
.lh-warnings__msg {
color: var(--toplevel-warning-message-text-color);
margin: 0;
}
.lh-warnings ul {
margin: 0;
}
.lh-warnings li {
margin: var(--item-margin) 0;
}
.lh-warnings li:last-of-type {
margin-bottom: 0;
}
.lh-scores-header {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.lh-scores-header__solo {
padding: 0;
border: 0;
}
/* Gauge */
.lh-gauge__wrapper--pass {
color: var(--color-pass);
fill: var(--color-pass);
stroke: var(--color-pass);
}
.lh-gauge__wrapper--average {
color: var(--color-average);
fill: var(--color-average);
stroke: var(--color-average);
}
.lh-gauge__wrapper--fail {
color: var(--color-fail);
fill: var(--color-fail);
stroke: var(--color-fail);
}
.lh-gauge__wrapper--not-applicable {
color: var(--color-not-applicable);
fill: var(--color-not-applicable);
stroke: var(--color-not-applicable);
}
.lh-gauge {
stroke-linecap: round;
width: var(--gauge-circle-size);
height: var(--gauge-circle-size);
}
.lh-category .lh-gauge {
--gauge-circle-size: var(--gauge-circle-size-big);
}
.lh-gauge-base {
opacity: 0.1;
stroke: var(--circle-background);
}
.lh-gauge-arc {
fill: none;
stroke: var(--circle-color);
transform-origin: 50% 50%;
animation: load-gauge var(--transition-length) ease forwards;
animation-delay: 250ms;
}
.lh-gauge__svg-wrapper {
position: relative;
height: var(--gauge-circle-size);
}
.lh-category .lh-gauge__svg-wrapper {
--gauge-circle-size: var(--gauge-circle-size-big);
}
/* The plugin badge overlay */
.lh-gauge__wrapper--plugin .lh-gauge__svg-wrapper::before {
width: var(--plugin-badge-size);
height: var(--plugin-badge-size);
background-color: var(--plugin-badge-background-color);
background-image: var(--plugin-icon-url);
background-repeat: no-repeat;
background-size: var(--plugin-icon-size);
background-position: 58% 50%;
content: "";
position: absolute;
right: -6px;
bottom: 0px;
display: block;
z-index: 100;
box-shadow: 0 0 4px rgba(0,0,0,.2);
border-radius: 25%;
}
.lh-category .lh-gauge__wrapper--plugin .lh-gauge__svg-wrapper::before {
width: var(--plugin-badge-size-big);
height: var(--plugin-badge-size-big);
}
@keyframes load-gauge {
from { stroke-dasharray: 0 352; }
}
.lh-gauge__percentage {
width: 100%;
height: var(--gauge-circle-size);
position: absolute;
font-family: var(--report-font-family-monospace);
font-size: calc(var(--gauge-circle-size) * 0.34 + 1.3px);
line-height: 0;
text-align: center;
top: calc(var(--score-container-padding) + var(--gauge-circle-size) / 2);
}
.lh-category .lh-gauge__percentage {
--gauge-circle-size: var(--gauge-circle-size-big);
--gauge-percentage-font-size: var(--gauge-percentage-font-size-big);
}
.lh-gauge__wrapper {
position: relative;
display: flex;
align-items: center;
flex-direction: column;
text-decoration: none;
padding: var(--score-container-padding);
--transition-length: 1s;
/* Contain the layout style paint & layers during animation*/
contain: content;
will-change: opacity; /* Only using for layer promotion */
}
.lh-gauge__label {
font-size: var(--gauge-label-font-size);
line-height: var(--gauge-label-line-height);
margin-top: 10px;
text-align: center;
color: var(--report-text-color);
}
/* TODO(#8185) use more BEM (.lh-gauge__label--big) instead of relying on descendant selector */
.lh-category .lh-gauge__label {
--gauge-label-font-size: var(--gauge-label-font-size-big);
--gauge-label-line-height: var(--gauge-label-line-height-big);
margin-top: 14px;
}
.lh-scores-header .lh-gauge__wrapper,
.lh-scores-header .lh-gauge--pwa__wrapper,
.lh-sticky-header .lh-gauge__wrapper,
.lh-sticky-header .lh-gauge--pwa__wrapper {
width: var(--gauge-wrapper-width);
}
.lh-scorescale {
display: inline-flex;
margin: 12px auto 0 auto;
border: 1px solid var(--color-gray-200);
border-radius: 20px;
padding: 8px 8px;
}
.lh-scorescale-range {
display: flex;
align-items: center;
margin: 0 12px;
font-family: var(--report-font-family-monospace);
white-space: nowrap;
}
/* Hide category score gauages if it's a single category report */
.lh-header--solo-category .lh-scores-wrapper {
display: none;
}
.lh-categories {
width: 100%;
overflow: hidden;
}
.lh-category {
padding: var(--category-padding);
max-width: var(--report-width);
margin: 0 auto;
}
.lh-category-wrapper {
border-bottom: 1px solid var(--color-gray-200);
}
.lh-category-wrapper:first-of-type {
border-top: 1px solid var(--color-gray-200);
}
/* section hash link jump should preserve fixed header
https://css-tricks.com/hash-tag-links-padding/
*/
.lh-category > .lh-permalink {
--sticky-header-height: calc(var(--gauge-circle-size) + var(--score-container-padding) * 2);
--topbar-plus-header: calc(var(--topbar-height) + var(--sticky-header-height));
margin-top: calc(var(--topbar-plus-header) * -1);
padding-bottom: var(--topbar-plus-header);
display: block;
visibility: hidden;
}
.lh-category-header {
font-size: var(--category-header-font-size);
min-height: var(--gauge-circle-size);
margin-bottom: var(--section-padding-vertical);
}
.lh-category-header .lh-score__gauge {
max-width: 400px;
width: auto;
margin: 0px auto;
}
.lh-category-header .lh-audit__title {
font-size: var(--category-header-font-size);
line-height: var(--header-line-height);
}
#lh-log {
position: fixed;
background-color: #323232;
color: #fff;
min-height: 48px;
min-width: 288px;
padding: 16px 24px;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);
border-radius: 2px;
margin: 12px;
font-size: 14px;
cursor: default;
transition: transform 0.3s, opacity 0.3s;
transform: translateY(100px);
opacity: 0;
bottom: 0;
left: 0;
z-index: 3;
}
#lh-log.show {
opacity: 1;
transform: translateY(0);
}
/* 964 fits the min-width of the filmstrip */
@media screen and (max-width: 964px) {
.lh-report {
margin-left: 0;
width: 100%;
}
}
@media print {
body {
-webkit-print-color-adjust: exact; /* print background colors */
}
.lh-container {
display: block;
}
.lh-report {
margin-left: 0;
padding-top: 0;
}
.lh-categories {
margin-top: 0;
}
}
.lh-table {
border-collapse: collapse;
/* Can't assign padding to table, so shorten the width instead. */
width: calc(100% - var(--audit-description-padding-left));
}
.lh-table thead th {
font-weight: normal;
color: var(--color-gray-600);
/* See text-wrapping comment on .lh-container. */
word-break: normal;
}
.lh-row--odd {
background-color: var(--table-higlight-background-color);
}
.lh-row--hidden {
display: none;
}
.lh-table th,
.lh-table td {
padding: 8px 6px;
}
.lh-table th:first-child {
padding-left: 0;
}
.lh-table th:last-child {
padding-right: 0;
}
.lh-table tr {
vertical-align: middle;
}
/* Looks unnecessary, but mostly for keeping the <th>s left-aligned */
.lh-table-column--text,
.lh-table-column--source-location,
.lh-table-column--url,
/* .lh-table-column--thumbnail, */
/* .lh-table-column--empty,*/
.lh-table-column--code,
.lh-table-column--node {
text-align: left;
}
.lh-table-column--code {
min-width: 100px;
}
.lh-table-column--bytes,
.lh-table-column--timespanMs,
.lh-table-column--ms,
.lh-table-column--numeric {
text-align: right;
word-break: normal;
}
.lh-table .lh-table-column--thumbnail {
width: var(--image-preview-size);
padding: 0;
}
.lh-table-column--url {
min-width: 250px;
}
.lh-table-column--text {
min-width: 80px;
}
/* Keep columns narrow if they follow the URL column */
/* 12% was determined to be a decent narrow width, but wide enough for column headings */
.lh-table-column--url + th.lh-table-column--bytes,
.lh-table-column--url + .lh-table-column--bytes + th.lh-table-column--bytes,
.lh-table-column--url + .lh-table-column--ms,
.lh-table-column--url + .lh-table-column--ms + th.lh-table-column--bytes,
.lh-table-column--url + .lh-table-column--bytes + th.lh-table-column--timespanMs {
width: 12%;
}
.lh-text__url-host {
display: inline;
}
.lh-text__url-host {
margin-left: calc(var(--report-font-size) / 2);
opacity: 0.6;
font-size: 90%
}
.lh-thumbnail {
object-fit: cover;
width: var(--image-preview-size);
height: var(--image-preview-size);
display: block;
}
.lh-unknown pre {
overflow: scroll;
border: solid 1px var(--color-gray-200);
}
.lh-text__url > a {
color: inherit;
text-decoration: none;
}
.lh-text__url > a:hover {
text-decoration: underline dotted #999;
}
.lh-sub-item-row {
margin-left: 20px;
margin-bottom: 0;
color: var(--color-gray-700);
}
.lh-sub-item-row td {
padding-top: 4px;
padding-bottom: 4px;
padding-left: 20px;
}
/* Chevron
https://codepen.io/paulirish/pen/LmzEmK
*/
.lh-chevron {
--chevron-angle: 42deg;
/* Edge doesn't support transform: rotate(calc(...)), so we define it here */
--chevron-angle-right: -42deg;
width: var(--chevron-size);
height: var(--chevron-size);
margin-top: calc((var(--report-line-height) - 12px) / 2);
}
.lh-chevron__lines {
transition: transform 0.4s;
transform: translateY(var(--report-line-height));
}
.lh-chevron__line {
stroke: var(--chevron-line-stroke);
stroke-width: var(--chevron-size);
stroke-linecap: square;
transform-origin: 50%;
transform: rotate(var(--chevron-angle));
transition: transform 300ms, stroke 300ms;
}
.lh-audit-group > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-right,
.lh-audit-group[open] > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-left,
.lh-audit > .lh-expandable-details .lh-chevron__line-right,
.lh-audit > .lh-expandable-details[open] .lh-chevron__line-left {
transform: rotate(var(--chevron-angle-right));
}
.lh-audit-group[open] > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__line-right,
.lh-audit > .lh-expandable-details[open] .lh-chevron__line-right {
transform: rotate(var(--chevron-angle));
}
.lh-audit-group[open] > summary > .lh-audit-group__summary > .lh-chevron .lh-chevron__lines,
.lh-audit > .lh-expandable-details[open] .lh-chevron__lines {
transform: translateY(calc(var(--chevron-size) * -1));
}
/* Tooltip */
.tooltip-boundary {
position: relative;
}
.tooltip {
position: absolute;
display: none; /* Don't retain these layers when not needed */
opacity: 0;
background: #ffffff;
min-width: 246px;
max-width: 275px;
padding: 15px;
border-radius: 5px;
text-align: initial;
}
/* shrink tooltips to not be cutoff on left edge of narrow viewports
45vw is chosen to be ~= width of the left column of metrics
*/
@media screen and (max-width: 535px) {
.tooltip {
min-width: 45vw;
padding: 3vw;
}
}
.tooltip-boundary:hover {
background-color: var(--color-hover);
}
.tooltip-boundary:hover .tooltip {
display: block;
animation: fadeInTooltip 250ms;
animation-fill-mode: forwards;
animation-delay: 850ms;
bottom: 100%;
z-index: 1;
will-change: opacity;
right: 0;
pointer-events: none;
}
.tooltip::before {
content: "";
border: solid transparent;
border-bottom-color: #fff;
border-width: 10px;
position: absolute;
bottom: -20px;
right: 6px;
transform: rotate(180deg);
pointer-events: none;
}
@keyframes fadeInTooltip {
0% { opacity: 0; }
75% { opacity: 1; }
100% { opacity: 1; filter: drop-shadow(1px 0px 1px #aaa) drop-shadow(0px 2px 4px hsla(206, 6%, 25%, 0.15)); pointer-events: auto; }
}
/* Element screenshot */
.lh-element-screenshot {
position: relative;
overflow: hidden;
float: left;
margin-right: 20px;
}
.lh-element-screenshot__content {
overflow: hidden;
}
.lh-element-screenshot__image {
/* Set by ElementScreenshotRenderer.installFullPageScreenshotCssVariable */
background-image: var(--element-screenshot-url);
outline: 2px solid #777;
background-color: white;
background-repeat: no-repeat;
}
.lh-element-screenshot__mask {
position: absolute;
background: #555;
opacity: 0.8;
}
.lh-element-screenshot__element-marker {
position: absolute;
outline: 2px solid var(--color-lime-400);
}
.lh-element-screenshot__overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2000; /* .lh-topbar is 1000 */
background: var(--screenshot-overlay-background);
display: flex;
align-items: center;
justify-content: center;
cursor: zoom-out;
}
.lh-element-screenshot__overlay .lh-element-screenshot {
margin-right: 0; /* clearing margin used in thumbnail case */
outline: 1px solid var(--color-gray-700);
}
.lh-screenshot-overlay--enabled .lh-element-screenshot {
cursor: zoom-out;
}
.lh-screenshot-overlay--enabled .lh-node .lh-element-screenshot {
cursor: zoom-in;
}
/*# sourceURL=report-styles.css */
</style>
</head>
<body class="lh-root lh-vars">
<noscript>Lighthouse report requires JavaScript. Please enable.</noscript>
<div hidden=""><!--
@license
Copyright 2018 The Lighthouse Authors. All Rights Reserved.
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
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.
-->
<!-- Lighthouse run warnings -->
<template id="tmpl-lh-warnings--toplevel">
<div class="lh-warnings lh-warnings--toplevel">
<p class="lh-warnings__msg"></p>
<ul></ul>
</div>
</template>
<!-- Lighthouse score scale -->
<template id="tmpl-lh-scorescale">
<div class="lh-scorescale">
<span class="lh-scorescale-range lh-scorescale-range--fail">049</span>
<span class="lh-scorescale-range lh-scorescale-range--average">5089</span>
<span class="lh-scorescale-range lh-scorescale-range--pass">90100</span>
</div>
</template>
<!-- Toggle arrow chevron -->
<template id="tmpl-lh-chevron">
<svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg>
</template>
<!-- Lighthouse category header -->
<template id="tmpl-lh-category-header">
<div class="lh-category-header">
<div class="lh-score__gauge" role="heading" aria-level="2"></div>
<div class="lh-category-header__description"></div>
</div>
</template>
<!-- Lighthouse clump -->
<template id="tmpl-lh-clump">
<!-- TODO: group classes shouldn't be reused for clumps. -->
<details class="lh-clump lh-audit-group">
<summary>
<div class="lh-audit-group__summary">
<div class="lh-audit-group__header">
<span class="lh-audit-group__title"></span>
<span class="lh-audit-group__itemcount"></span>
<!-- .lh-audit-group__description will be added here -->
<!-- .lh-metrics-toggle will be added here -->
</div>
<div class=""></div>
</div>
</summary>
</details>
</template>
<!-- Lighthouse metrics toggle -->
<template id="tmpl-lh-metrics-toggle">
<div class="lh-metrics-toggle">
<input class="lh-metrics-toggle__input" type="checkbox" id="toggle-metric-descriptions" aria-label="Toggle the display of metric descriptions">
<label class="lh-metrics-toggle__label" for="toggle-metric-descriptions">
<div class="lh-metrics-toggle__icon lh-metrics-toggle__icon--less" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">
<path class="lh-metrics-toggle__lines" d="M4 9h16v2H4zm0 4h10v2H4z"></path>
</svg>
</div>
<div class="lh-metrics-toggle__icon lh-metrics-toggle__icon--more" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path class="lh-metrics-toggle__lines" d="M3 18h12v-2H3v2zM3 6v2h18V6H3zm0 7h18v-2H3v2z"></path>
</svg>
</div>
</label>
</div>
</template>
<!-- Lighthouse audit -->
<template id="tmpl-lh-audit">
<div class="lh-audit">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"></div>
</div>
</summary>
<div class="lh-audit__description"></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div>
</template>
<!-- Lighthouse perf metric -->
<template id="tmpl-lh-metric">
<div class="lh-metric">
<div class="lh-metric__innerwrap">
<span class="lh-metric__title"></span>
<div class="lh-metric__value"></div>
<div class="lh-metric__description"></div>
</div>
</div>
</template>
<!-- Lighthouse perf opportunity -->
<template id="tmpl-lh-opportunity">
<div class="lh-audit lh-audit--load-opportunity">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<div class="lh-load-opportunity__cols">
<div class="lh-load-opportunity__col lh-load-opportunity__col--one">
<span class="lh-audit__score-icon"></span>
<div class="lh-audit__title"></div>
</div>
<div class="lh-load-opportunity__col lh-load-opportunity__col--two">
<div class="lh-load-opportunity__sparkline">
<div class="lh-sparkline"><div class="lh-sparkline__bar"></div></div>
</div>
<div class="lh-audit__display-text"></div>
<div class="lh-chevron-container"></div>
</div>
</div>
</div>
</summary>
<div class="lh-audit__description"></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div>
</template>
<!-- Lighthouse perf opportunity header -->
<template id="tmpl-lh-opportunity-header">
<div class="lh-load-opportunity__header lh-load-opportunity__cols">
<div class="lh-load-opportunity__col lh-load-opportunity__col--one"></div>
<div class="lh-load-opportunity__col lh-load-opportunity__col--two"></div>
</div>
</template>
<!-- Lighthouse score container -->
<template id="tmpl-lh-scores-wrapper">
<style>
.lh-scores-container {
display: flex;
flex-direction: column;
padding: var(--scores-container-padding);
position: relative;
width: 100%;
}
.lh-sticky-header {
--gauge-circle-size: 36px;
--plugin-badge-size: 18px;
--plugin-icon-size: 75%;
--gauge-wrapper-width: 60px;
--gauge-percentage-font-size: 13px;
position: fixed;
left: 0;
right: 0;
top: var(--topbar-height);
font-weight: 700;
display: none;
justify-content: center;
background-color: var(--sticky-header-background-color);
border-bottom: 1px solid var(--color-gray-200);
padding-top: var(--score-container-padding);
padding-bottom: 4px;
z-index: 1;
pointer-events: none;
}
.lh-devtools .lh-sticky-header {
/* The report within DevTools is placed in a container with overflow, which changes the placement of this header unless we change `position` to `sticky.` */
position: sticky;
}
.lh-sticky-header--visible {
display: grid;
grid-auto-flow: column;
pointer-events: auto;
}
/* Disable the gauge arc animation for the sticky header, so toggling display: none
does not play the animation. */
.lh-sticky-header .lh-gauge-arc {
animation: none;
}
.lh-sticky-header .lh-gauge__label {
display: none;
}
.lh-highlighter {
width: var(--gauge-wrapper-width);
height: 1px;
background-color: var(--highlighter-background-color);
/* Position at bottom of first gauge in sticky header. */
position: absolute;
grid-column: 1;
bottom: -1px;
}
.lh-gauge__wrapper:first-of-type {
contain: none;
}
</style>
<div class="lh-scores-wrapper">
<div class="lh-scores-container">
<div class="pyro">
<div class="before"></div>
<div class="after"></div>
</div>
</div>
</div>
</template>
<!-- Lighthouse topbar -->
<template id="tmpl-lh-topbar">
<style>
.lh-topbar {
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 1000;
display: flex;
align-items: center;
height: var(--topbar-height);
background-color: var(--topbar-background-color);
padding: var(--topbar-padding);
}
.lh-topbar__logo {
width: var(--topbar-logo-size);
height: var(--topbar-logo-size);
user-select: none;
flex: none;
}
.lh-topbar__logo .shape {
fill: var(--report-text-color);
}
.lh-topbar__url {
margin: var(--topbar-padding);
text-decoration: none;
color: var(--report-text-color);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.lh-tools {
margin-left: auto;
will-change: transform;
min-width: var(--report-icon-size);
}
.lh-tools__button {
width: var(--report-icon-size);
height: var(--report-icon-size);
cursor: pointer;
margin-right: 5px;
/* This is actually a button element, but we want to style it like a transparent div. */
display: flex;
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
outline: inherit;
}
.lh-tools__button svg {
fill: var(--tools-icon-color);
}
.dark .lh-tools__button svg {
filter: invert(1);
}
.lh-tools__button.active + .lh-tools__dropdown {
opacity: 1;
clip: rect(-1px, 194px, 242px, -3px);
visibility: visible;
}
.lh-tools__dropdown {
position: absolute;
background-color: var(--report-background-color);
border: 1px solid var(--report-border-color);
border-radius: 3px;
padding: calc(var(--default-padding) / 2) 0;
cursor: pointer;
top: 36px;
right: 0;
box-shadow: 1px 1px 3px #ccc;
min-width: 125px;
clip: rect(0, 164px, 0, 0);
visibility: hidden;
opacity: 0;
transition: all 200ms cubic-bezier(0,0,0.2,1);
}
.lh-tools__dropdown a {
color: currentColor;
text-decoration: none;
white-space: nowrap;
padding: 0 12px;
line-height: 2;
}
.lh-tools__dropdown a:hover,
.lh-tools__dropdown a:focus {
background-color: var(--color-gray-200);
outline: none;
}
/* save-gist option hidden in report. */
.lh-tools__dropdown a[data-action='save-gist'] {
display: none;
}
@media screen and (max-width: 964px) {
.lh-tools__dropdown {
right: 0;
left: initial;
}
}
@media print {
.lh-topbar {
position: static;
margin-left: 0;
}
.lh-tools__dropdown {
display: none;
}
}
</style>
<div class="lh-topbar">
<!-- Lighthouse logo. -->
<svg class="lh-topbar__logo" viewBox="0 0 24 24">
<defs>
<linearGradient x1="57.456%" y1="13.086%" x2="18.259%" y2="72.322%" id="lh-topbar__logo--a">
<stop stop-color="#262626" stop-opacity=".1" offset="0%"></stop>
<stop stop-color="#262626" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<linearGradient x1="100%" y1="50%" x2="0%" y2="50%" id="lh-topbar__logo--b">
<stop stop-color="#262626" stop-opacity=".1" offset="0%"></stop>
<stop stop-color="#262626" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<linearGradient x1="58.764%" y1="65.756%" x2="36.939%" y2="50.14%" id="lh-topbar__logo--c">
<stop stop-color="#262626" stop-opacity=".1" offset="0%"></stop>
<stop stop-color="#262626" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<linearGradient x1="41.635%" y1="20.358%" x2="72.863%" y2="85.424%" id="lh-topbar__logo--d">
<stop stop-color="#FFF" stop-opacity=".1" offset="0%"></stop>
<stop stop-color="#FFF" stop-opacity="0" offset="100%"></stop>
</linearGradient>
</defs>
<g fill="none" fill-rule="evenodd">
<path d="M12 3l4.125 2.625v3.75H18v2.25h-1.688l1.5 9.375H6.188l1.5-9.375H6v-2.25h1.875V5.648L12 3zm2.201 9.938L9.54 14.633 9 18.028l5.625-2.062-.424-3.028zM12.005 5.67l-1.88 1.207v2.498h3.75V6.86l-1.87-1.19z" fill="#F44B21"></path>
<path fill="#FFF" d="M14.201 12.938L9.54 14.633 9 18.028l5.625-2.062z"></path>
<path d="M6 18c-2.042 0-3.95-.01-5.813 0l1.5-9.375h4.326L6 18z" fill="url(#lh-topbar__logo--a)" fill-rule="nonzero" transform="translate(6 3)"></path>
<path fill="#FFF176" fill-rule="nonzero" d="M13.875 9.375v-2.56l-1.87-1.19-1.88 1.207v2.543z"></path>
<path fill="url(#lh-topbar__logo--b)" fill-rule="nonzero" d="M0 6.375h6v2.25H0z" transform="translate(6 3)"></path>
<path fill="url(#lh-topbar__logo--c)" fill-rule="nonzero" d="M6 6.375H1.875v-3.75L6 0z" transform="translate(6 3)"></path>
<path fill="url(#lh-topbar__logo--d)" fill-rule="nonzero" d="M6 0l4.125 2.625v3.75H12v2.25h-1.688l1.5 9.375H.188l1.5-9.375H0v-2.25h1.875V2.648z" transform="translate(6 3)"></path>
</g>
</svg>
<a href="" class="lh-topbar__url" target="_blank" rel="noopener"></a>
<div class="lh-tools">
<button id="lh-tools-button" class="lh-tools__button" title="Tools menu" aria-label="Toggle report tools menu" aria-haspopup="menu" aria-expanded="false" aria-controls="lh-tools-dropdown">
<svg width="100%" height="100%" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"></path>
</svg>
</button>
<div id="lh-tools-dropdown" role="menu" class="lh-tools__dropdown" aria-labelledby="lh-tools-button">
<a role="menuitem" tabindex="-1" href="#" class="report-icon report-icon--print" data-i18n="dropdownPrintSummary" data-action="print-summary"></a>
<a role="menuitem" tabindex="-1" href="#" class="report-icon report-icon--print" data-i18n="dropdownPrintExpanded" data-action="print-expanded"></a>
<a role="menuitem" tabindex="-1" href="#" class="report-icon report-icon--copy" data-i18n="dropdownCopyJSON" data-action="copy"></a>
<a role="menuitem" tabindex="-1" href="#" class="report-icon report-icon--download" data-i18n="dropdownSaveHTML" data-action="save-html"></a>
<a role="menuitem" tabindex="-1" href="#" class="report-icon report-icon--download" data-i18n="dropdownSaveJSON" data-action="save-json"></a>
<a role="menuitem" tabindex="-1" href="#" class="report-icon report-icon--open" data-i18n="dropdownViewer" data-action="open-viewer"></a>
<a role="menuitem" tabindex="-1" href="#" class="report-icon report-icon--open" data-i18n="dropdownSaveGist" data-action="save-gist"></a>
<a role="menuitem" tabindex="-1" href="#" class="report-icon report-icon--dark" data-i18n="dropdownDarkTheme" data-action="toggle-dark"></a>
</div>
</div>
</div>
</template>
<!-- Lighthouse header -->
<template id="tmpl-lh-heading">
<style>
/* CSS Fireworks. Originally by Eddie Lin
https://codepen.io/paulirish/pen/yEVMbP
*/
.pyro {
display: none;
z-index: 1;
pointer-events: none;
}
.score100 .pyro {
display: block;
}
.score100 .lh-lighthouse stop:first-child {
stop-color: hsla(200, 12%, 95%, 0);
}
.score100 .lh-lighthouse stop:last-child {
stop-color: hsla(65, 81%, 76%, 1);
}
.pyro > .before, .pyro > .after {
position: absolute;
width: 5px;
height: 5px;
border-radius: 2.5px;
box-shadow: 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff;
animation: 1s bang ease-out infinite backwards, 1s gravity ease-in infinite backwards, 5s position linear infinite backwards;
animation-delay: 1s, 1s, 1s;
}
.pyro > .after {
animation-delay: 2.25s, 2.25s, 2.25s;
animation-duration: 1.25s, 1.25s, 6.25s;
}
.fireworks-paused .pyro > div {
animation-play-state: paused;
}
@keyframes bang {
to {
box-shadow: -70px -115.67px #47ebbc, -28px -99.67px #eb47a4, 58px -31.67px #7eeb47, 13px -141.67px #eb47c5, -19px 6.33px #7347eb, -2px -74.67px #ebd247, 24px -151.67px #eb47e0, 57px -138.67px #b4eb47, -51px -104.67px #479eeb, 62px 8.33px #ebcf47, -93px 0.33px #d547eb, -16px -118.67px #47bfeb, 53px -84.67px #47eb83, 66px -57.67px #eb47bf, -93px -65.67px #91eb47, 30px -13.67px #86eb47, -2px -59.67px #83eb47, -44px 1.33px #eb47eb, 61px -58.67px #47eb73, 5px -22.67px #47e8eb, -66px -28.67px #ebe247, 42px -123.67px #eb5547, -75px 26.33px #7beb47, 15px -52.67px #a147eb, 36px -51.67px #eb8347, -38px -12.67px #eb5547, -46px -59.67px #47eb81, 78px -114.67px #eb47ba, 15px -156.67px #eb47bf, -36px 1.33px #eb4783, -72px -86.67px #eba147, 31px -46.67px #ebe247, -68px 29.33px #47e2eb, -55px 19.33px #ebe047, -56px 27.33px #4776eb, -13px -91.67px #eb5547, -47px -138.67px #47ebc7, -18px -96.67px #eb47ac, 11px -88.67px #4783eb, -67px -28.67px #47baeb, 53px 10.33px #ba47eb, 11px 19.33px #5247eb, -5px -11.67px #eb4791, -68px -4.67px #47eba7, 95px -37.67px #eb478b, -67px -162.67px #eb5d47, -54px -120.67px #eb6847, 49px -12.67px #ebe047, 88px 8.33px #47ebda, 97px 33.33px #eb8147, 6px -71.67px #ebbc47;
}
}
@keyframes gravity {
to {
transform: translateY(80px);
opacity: 0;
}
}
@keyframes position {
0%, 19.9% {
margin-top: 4%;
margin-left: 47%;
}
20%, 39.9% {
margin-top: 7%;
margin-left: 30%;
}
40%, 59.9% {
margin-top: 6%;
margin-left: 70%;
}
60%, 79.9% {
margin-top: 3%;
margin-left: 20%;
}
80%, 99.9% {
margin-top: 3%;
margin-left: 80%;
}
}
</style>
<div class="lh-header-container">
<div class="lh-scores-wrapper-placeholder"></div>
</div>
</template>
<!-- Lighthouse footer -->
<template id="tmpl-lh-footer">
<style>
.lh-footer {
padding: var(--footer-padding-vertical) calc(var(--default-padding) * 2);
max-width: var(--report-width);
margin: 0 auto;
}
.lh-footer .lh-generated {
text-align: center;
}
.lh-env__title {
font-size: var(--env-item-font-size-big);
line-height: var(--env-item-line-height-big);
text-align: center;
padding: var(--score-container-padding);
}
.lh-env {
padding: var(--default-padding) 0;
}
.lh-env__items {
padding-left: 16px;
margin: 0 0 var(--audits-margin-bottom);
padding: 0;
}
.lh-env__items .lh-env__item:nth-child(2n) {
background-color: var(--env-item-background-color);
}
.lh-env__item {
display: flex;
padding: var(--env-item-padding);
position: relative;
}
span.lh-env__name {
font-weight: bold;
min-width: var(--env-name-min-width);
flex: 0.5;
padding: 0 8px;
}
span.lh-env__description {
text-align: left;
flex: 1;
}
</style>
<footer class="lh-footer">
<!-- TODO(i18n): localize runtime settings -->
<div class="lh-env">
<div class="lh-env__title">Runtime Settings</div>
<ul class="lh-env__items">
<template id="tmpl-lh-env__items">
<li class="lh-env__item">
<span class="lh-env__name"></span>
<span class="lh-env__description"></span>
</li>
</template>
</ul>
</div>
<div class="lh-generated">
<!-- TODO(i18n): use ICU replacement to replace version w/o concatenation. -->
Generated by <b>Lighthouse</b> <span class="lh-footer__version"></span> |
<a href="https://github.com/GoogleChrome/Lighthouse/issues" target="_blank" rel="noopener" class="lh-footer__version_issue">File an issue</a>
</div>
</footer>
</template>
<!-- Lighthouse score gauge -->
<template id="tmpl-lh-gauge">
<a href="#" class="lh-gauge__wrapper">
<!-- Wrapper exists for the ::before plugin icon. Cannot create pseudo-elements on svgs. -->
<div class="lh-gauge__svg-wrapper">
<svg viewBox="0 0 120 120" class="lh-gauge">
<circle class="lh-gauge-base" r="56" cx="60" cy="60" stroke-width="8"></circle>
<circle class="lh-gauge-arc" r="56" cx="60" cy="60" stroke-width="8"></circle>
</svg>
</div>
<div class="lh-gauge__percentage"></div>
<!-- TODO: should likely be an h2 -->
<div class="lh-gauge__label"></div>
</a>
</template>
<!-- Lighthouse PWA badge gauge -->
<template id="tmpl-lh-gauge--pwa">
<style>
.lh-gauge--pwa .lh-gauge--pwa__component {
display: none;
}
.lh-gauge--pwa__wrapper:not(.lh-badged--all) .lh-gauge--pwa__logo > path {
/* Gray logo unless everything is passing. */
fill: #B0B0B0;
}
.lh-gauge--pwa__disc {
fill: var(--color-gray-200);
}
.lh-gauge--pwa__logo--primary-color {
fill: #304FFE;
}
.lh-gauge--pwa__logo--secondary-color {
fill: #3D3D3D;
}
.dark .lh-gauge--pwa__logo--secondary-color {
fill: #D8B6B6;
}
/* No passing groups. */
.lh-gauge--pwa__wrapper:not([class*='lh-badged--']) .lh-gauge--pwa__na-line {
display: inline;
}
/* Just optimized. Same n/a line as no passing groups. */
.lh-gauge--pwa__wrapper.lh-badged--pwa-optimized:not(.lh-badged--pwa-installable) .lh-gauge--pwa__na-line {
display: inline;
}
/* Just installable. */
.lh-gauge--pwa__wrapper.lh-badged--pwa-installable .lh-gauge--pwa__installable-badge {
display: inline;
}
/* All passing groups. */
.lh-gauge--pwa__wrapper.lh-badged--all .lh-gauge--pwa__check-circle {
display: inline;
}
</style>
<a href="#" class="lh-gauge__wrapper lh-gauge--pwa__wrapper">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" class="lh-gauge lh-gauge--pwa">
<defs>
<linearGradient id="lh-gauge--pwa__check-circle__gradient" x1="50%" y1="0%" x2="50%" y2="100%">
<stop stop-color="#00C852" offset="0%"></stop>
<stop stop-color="#009688" offset="100%"></stop>
</linearGradient>
<linearGradient id="lh-gauge--pwa__installable__shadow-gradient" x1="76.056%" x2="24.111%" y1="82.995%" y2="24.735%">
<stop stop-color="#A5D6A7" offset="0%"></stop>
<stop stop-color="#80CBC4" offset="100%"></stop>
</linearGradient>
<g id="lh-gauge--pwa__installable-badge">
<circle fill="#FFFFFF" cx="10" cy="10" r="10"></circle>
<path fill="#009688" d="M10 4.167A5.835 5.835 0 0 0 4.167 10 5.835 5.835 0 0 0 10 15.833 5.835 5.835 0 0 0 15.833 10 5.835 5.835 0 0 0 10 4.167zm2.917 6.416h-2.334v2.334H9.417v-2.334H7.083V9.417h2.334V7.083h1.166v2.334h2.334v1.166z"></path>
</g>
</defs>
<g stroke="none" fill-rule="nonzero">
<!-- Background and PWA logo (color by default) -->
<circle class="lh-gauge--pwa__disc" cx="30" cy="30" r="30"></circle>
<g class="lh-gauge--pwa__logo">
<path class="lh-gauge--pwa__logo--secondary-color" d="M35.66 19.39l.7-1.75h2L37.4 15 38.6 12l3.4 9h-2.51l-.58-1.61z"></path>
<path class="lh-gauge--pwa__logo--primary-color" d="M33.52 21l3.65-9h-2.42l-2.5 5.82L30.5 12h-1.86l-1.9 5.82-1.35-2.65-1.21 3.72L25.4 21h2.38l1.72-5.2 1.64 5.2z"></path>
<path class="lh-gauge--pwa__logo--secondary-color" fill-rule="nonzero" d="M20.3 17.91h1.48c.45 0 .85-.05 1.2-.15l.39-1.18 1.07-3.3a2.64 2.64 0 0 0-.28-.37c-.55-.6-1.36-.91-2.42-.91H18v9h2.3V17.9zm1.96-3.84c.22.22.33.5.33.87 0 .36-.1.65-.29.87-.2.23-.59.35-1.15.35h-.86v-2.41h.87c.52 0 .89.1 1.1.32z"></path>
</g>
<!-- No badges. -->
<rect class="lh-gauge--pwa__component lh-gauge--pwa__na-line" fill="#FFFFFF" x="20" y="32" width="20" height="4" rx="2"></rect>
<!-- Just installable. -->
<g class="lh-gauge--pwa__component lh-gauge--pwa__installable-badge" transform="translate(20, 29)">
<path fill="url(#lh-gauge--pwa__installable__shadow-gradient)" d="M33.629 19.487c-4.272 5.453-10.391 9.39-17.415 10.869L3 17.142 17.142 3 33.63 19.487z"></path>
<use href="#lh-gauge--pwa__installable-badge"></use>
</g>
<!-- Full PWA. -->
<g class="lh-gauge--pwa__component lh-gauge--pwa__check-circle" transform="translate(18, 28)">
<circle fill="#FFFFFF" cx="12" cy="12" r="12"></circle>
<path fill="url(#lh-gauge--pwa__check-circle__gradient)" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"></path>
</g>
</g>
</svg>
<div class="lh-gauge__label"></div>
</a>
</template>
<!-- Lighthouse crtiical request chains component -->
<template id="tmpl-lh-crc">
<div class="lh-crc-container">
<style>
.lh-crc .tree-marker {
width: 12px;
height: 26px;
display: block;
float: left;
background-position: top left;
}
.lh-crc .horiz-down {
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>');
}
.lh-crc .right {
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>');
}
.lh-crc .up-right {
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>');
}
.lh-crc .vert-right {
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>');
}
.lh-crc .vert {
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>');
}
.lh-crc .crc-tree {
font-size: 14px;
width: 100%;
overflow-x: auto;
}
.lh-crc .crc-node {
height: 26px;
line-height: 26px;
white-space: nowrap;
}
.lh-crc .crc-node__tree-value {
margin-left: 10px;
}
.lh-crc .crc-node__tree-value div {
display: inline;
}
.lh-crc .crc-node__chain-duration {
font-weight: 700;
}
.lh-crc .crc-initial-nav {
color: #595959;
font-style: italic;
}
.lh-crc__summary-value {
margin-bottom: 10px;
}
</style>
<div>
<div class="lh-crc__summary-value">
<span class="lh-crc__longest_duration_label"></span> <b class="lh-crc__longest_duration"></b>
</div>
</div>
<div class="lh-crc">
<div class="crc-initial-nav"></div>
<!-- stamp for each chain -->
<template id="tmpl-lh-crc__chains">
<div class="crc-node">
<span class="crc-node__tree-marker">
</span>
<span class="crc-node__tree-value">
</span>
</div>
</template>
</div>
</div>
</template>
<template id="tmpl-lh-3p-filter">
<style>
.lh-3p-filter {
background-color: var(--table-higlight-background-color);
color: var(--color-gray-600);
float: right;
padding: 6px;
}
.lh-3p-filter-label, .lh-3p-filter-input {
vertical-align: middle;
user-select: none;
}
.lh-3p-filter-input:disabled + .lh-3p-ui-string {
text-decoration: line-through;
}
</style>
<div class="lh-3p-filter">
<label class="lh-3p-filter-label">
<input type="checkbox" class="lh-3p-filter-input" checked="">
<span class="lh-3p-ui-string">Show 3rd party resources</span> (<span class="lh-3p-filter-count"></span>)
</label>
</div>
</template>
<!-- Lighthouse snippet component -->
<template id="tmpl-lh-snippet">
<div class="lh-snippet">
<style>
:root {
--snippet-highlight-light: #fbf1f2;
--snippet-highlight-dark: #ffd6d8;
}
.lh-snippet__header {
position: relative;
overflow: hidden;
padding: 10px;
border-bottom: none;
color: var(--snippet-color);
background-color: var(--snippet-background-color);
border: 1px solid var(--report-border-color-secondary);
}
.lh-snippet__title {
font-weight: bold;
float: left;
}
.lh-snippet__node {
float: left;
margin-left: 4px;
}
.lh-snippet__toggle-expand {
padding: 1px 7px;
margin-top: -1px;
margin-right: -7px;
float: right;
background: transparent;
border: none;
cursor: pointer;
font-size: 14px;
color: #0c50c7;
}
.lh-snippet__snippet {
overflow: auto;
border: 1px solid var(--report-border-color-secondary);
}
/* Container needed so that all children grow to the width of the scroll container */
.lh-snippet__snippet-inner {
display: inline-block;
min-width: 100%;
}
.lh-snippet:not(.lh-snippet--expanded) .lh-snippet__show-if-expanded {
display: none;
}
.lh-snippet.lh-snippet--expanded .lh-snippet__show-if-collapsed {
display: none;
}
.lh-snippet__line {
background: white;
white-space: pre;
display: flex;
}
.lh-snippet__line:not(.lh-snippet__line--message):first-child {
padding-top: 4px;
}
.lh-snippet__line:not(.lh-snippet__line--message):last-child {
padding-bottom: 4px;
}
.lh-snippet__line--content-highlighted {
background: var(--snippet-highlight-dark);
}
.lh-snippet__line--message {
background: var(--snippet-highlight-light);
}
.lh-snippet__line--message .lh-snippet__line-number {
padding-top: 10px;
padding-bottom: 10px;
}
.lh-snippet__line--message code {
padding: 10px;
padding-left: 5px;
color: var(--color-fail);
font-family: var(--report-font-family);
}
.lh-snippet__line--message code {
white-space: normal;
}
.lh-snippet__line-icon {
padding-top: 10px;
display: none;
}
.lh-snippet__line--message .lh-snippet__line-icon {
display: block;
}
.lh-snippet__line-icon:before {
content: "";
display: inline-block;
vertical-align: middle;
margin-right: 4px;
width: var(--score-icon-size);
height: var(--score-icon-size);
background-image: var(--fail-icon-url);
}
.lh-snippet__line-number {
flex-shrink: 0;
width: 40px;
text-align: right;
font-family: monospace;
padding-right: 5px;
margin-right: 5px;
color: var(--color-gray-600);
user-select: none;
}
</style>
<template id="tmpl-lh-snippet__header">
<div class="lh-snippet__header">
<div class="lh-snippet__title"></div>
<div class="lh-snippet__node"></div>
<button class="lh-snippet__toggle-expand">
<span class="lh-snippet__btn-label-collapse lh-snippet__show-if-expanded"></span>
<span class="lh-snippet__btn-label-expand lh-snippet__show-if-collapsed"></span>
</button>
</div>
</template>
<template id="tmpl-lh-snippet__content">
<div class="lh-snippet__snippet">
<div class="lh-snippet__snippet-inner"></div>
</div>
</template>
<template id="tmpl-lh-snippet__line">
<div class="lh-snippet__line">
<div class="lh-snippet__line-number"></div>
<div class="lh-snippet__line-icon"></div>
<code></code>
</div>
</template>
</div>
</template>
<!-- Lighthouse element screenshot -->
<template id="tmpl-lh-element-screenshot">
<div class="lh-element-screenshot">
<div class="lh-element-screenshot__content">
<div class="lh-element-screenshot__mask">
<svg xmlns="http://www.w3.org/2000/svg" height="0" width="0">
<defs>
<clipPath clipPathUnits="objectBoundingBox"></clipPath>
<!-- clipPath filled by ElementScreenshotRenderer.renderClipPath -->
</defs>
</svg>
</div>
<div class="lh-element-screenshot__image"></div>
<div class="lh-element-screenshot__element-marker"></div>
</div>
</div>
</template>
</div>
<main>
<style>
.lh-topbar {
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 1000;
display: flex;
align-items: center;
height: var(--topbar-height);
background-color: var(--topbar-background-color);
padding: var(--topbar-padding);
}
.lh-topbar__logo {
width: var(--topbar-logo-size);
height: var(--topbar-logo-size);
user-select: none;
flex: none;
}
.lh-topbar__logo .shape {
fill: var(--report-text-color);
}
.lh-topbar__url {
margin: var(--topbar-padding);
text-decoration: none;
color: var(--report-text-color);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.lh-tools {
margin-left: auto;
will-change: transform;
min-width: var(--report-icon-size);
}
.lh-tools__button {
width: var(--report-icon-size);
height: var(--report-icon-size);
cursor: pointer;
margin-right: 5px;
/* This is actually a button element, but we want to style it like a transparent div. */
display: flex;
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
outline: inherit;
}
.lh-tools__button svg {
fill: var(--tools-icon-color);
}
.dark .lh-tools__button svg {
filter: invert(1);
}
.lh-tools__button.active + .lh-tools__dropdown {
opacity: 1;
clip: rect(-1px, 194px, 242px, -3px);
visibility: visible;
}
.lh-tools__dropdown {
position: absolute;
background-color: var(--report-background-color);
border: 1px solid var(--report-border-color);
border-radius: 3px;
padding: calc(var(--default-padding) / 2) 0;
cursor: pointer;
top: 36px;
right: 0;
box-shadow: 1px 1px 3px #ccc;
min-width: 125px;
clip: rect(0, 164px, 0, 0);
visibility: hidden;
opacity: 0;
transition: all 200ms cubic-bezier(0,0,0.2,1);
}
.lh-tools__dropdown a {
color: currentColor;
text-decoration: none;
white-space: nowrap;
padding: 0 12px;
line-height: 2;
}
.lh-tools__dropdown a:hover,
.lh-tools__dropdown a:focus {
background-color: var(--color-gray-200);
outline: none;
}
/* save-gist option hidden in report. */
.lh-tools__dropdown a[data-action='save-gist'] {
display: none;
}
@media screen and (max-width: 964px) {
.lh-tools__dropdown {
right: 0;
left: initial;
}
}
@media print {
.lh-topbar {
position: static;
margin-left: 0;
}
.lh-tools__dropdown {
display: none;
}
}
</style>
<div class="lh-topbar">
<!-- Lighthouse logo. -->
<svg class="lh-topbar__logo" viewBox="0 0 24 24">
<defs>
<linearGradient x1="57.456%" y1="13.086%" x2="18.259%" y2="72.322%" id="lh-topbar__logo--a">
<stop stop-color="#262626" stop-opacity=".1" offset="0%"></stop>
<stop stop-color="#262626" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<linearGradient x1="100%" y1="50%" x2="0%" y2="50%" id="lh-topbar__logo--b">
<stop stop-color="#262626" stop-opacity=".1" offset="0%"></stop>
<stop stop-color="#262626" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<linearGradient x1="58.764%" y1="65.756%" x2="36.939%" y2="50.14%" id="lh-topbar__logo--c">
<stop stop-color="#262626" stop-opacity=".1" offset="0%"></stop>
<stop stop-color="#262626" stop-opacity="0" offset="100%"></stop>
</linearGradient>
<linearGradient x1="41.635%" y1="20.358%" x2="72.863%" y2="85.424%" id="lh-topbar__logo--d">
<stop stop-color="#FFF" stop-opacity=".1" offset="0%"></stop>
<stop stop-color="#FFF" stop-opacity="0" offset="100%"></stop>
</linearGradient>
</defs>
<g fill="none" fill-rule="evenodd">
<path d="M12 3l4.125 2.625v3.75H18v2.25h-1.688l1.5 9.375H6.188l1.5-9.375H6v-2.25h1.875V5.648L12 3zm2.201 9.938L9.54 14.633 9 18.028l5.625-2.062-.424-3.028zM12.005 5.67l-1.88 1.207v2.498h3.75V6.86l-1.87-1.19z" fill="#F44B21"></path>
<path fill="#FFF" d="M14.201 12.938L9.54 14.633 9 18.028l5.625-2.062z"></path>
<path d="M6 18c-2.042 0-3.95-.01-5.813 0l1.5-9.375h4.326L6 18z" fill="url(#lh-topbar__logo--a)" fill-rule="nonzero" transform="translate(6 3)"></path>
<path fill="#FFF176" fill-rule="nonzero" d="M13.875 9.375v-2.56l-1.87-1.19-1.88 1.207v2.543z"></path>
<path fill="url(#lh-topbar__logo--b)" fill-rule="nonzero" d="M0 6.375h6v2.25H0z" transform="translate(6 3)"></path>
<path fill="url(#lh-topbar__logo--c)" fill-rule="nonzero" d="M6 6.375H1.875v-3.75L6 0z" transform="translate(6 3)"></path>
<path fill="url(#lh-topbar__logo--d)" fill-rule="nonzero" d="M6 0l4.125 2.625v3.75H12v2.25h-1.688l1.5 9.375H.188l1.5-9.375H0v-2.25h1.875V2.648z" transform="translate(6 3)"></path>
</g>
</svg>
<a href="http://localhost:8008/congo/samples/emoji/" class="lh-topbar__url" target="_blank" rel="noopener" title="http://localhost:8008/congo/samples/emoji/">http://localhost:8008/congo/samples/emoji/</a>
<div class="lh-tools">
<button id="lh-tools-button" class="lh-tools__button" title="Tools menu" aria-label="Toggle report tools menu" aria-haspopup="menu" aria-expanded="false" aria-controls="lh-tools-dropdown">
<svg width="100%" height="100%" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"></path>
</svg>
</button>
<div id="lh-tools-dropdown" role="menu" class="lh-tools__dropdown" aria-labelledby="lh-tools-button">
<a role="menuitem" tabindex="-1" href="#" class="report-icon report-icon--print" data-i18n="dropdownPrintSummary" data-action="print-summary">Print Summary</a>
<a role="menuitem" tabindex="-1" href="#" class="report-icon report-icon--print" data-i18n="dropdownPrintExpanded" data-action="print-expanded">Print Expanded</a>
<a role="menuitem" tabindex="-1" href="#" class="report-icon report-icon--copy" data-i18n="dropdownCopyJSON" data-action="copy">Copy JSON</a>
<a role="menuitem" tabindex="-1" href="#" class="report-icon report-icon--download" data-i18n="dropdownSaveHTML" data-action="save-html">Save as HTML</a>
<a role="menuitem" tabindex="-1" href="#" class="report-icon report-icon--download" data-i18n="dropdownSaveJSON" data-action="save-json">Save as JSON</a>
<a role="menuitem" tabindex="-1" href="#" class="report-icon report-icon--open" data-i18n="dropdownViewer" data-action="open-viewer">Open in Viewer</a>
<a role="menuitem" tabindex="-1" href="#" class="report-icon report-icon--open" data-i18n="dropdownSaveGist" data-action="save-gist">Save as Gist</a>
<a role="menuitem" tabindex="-1" href="#" class="report-icon report-icon--dark" data-i18n="dropdownDarkTheme" data-action="toggle-dark">Toggle Dark Theme</a>
</div>
</div>
</div>
<div class="lh-container"><div class="lh-sticky-header">
<a href="#performance" class="lh-gauge__wrapper lh-gauge__wrapper--pass">
<!-- Wrapper exists for the ::before plugin icon. Cannot create pseudo-elements on svgs. -->
<div class="lh-gauge__svg-wrapper">
<svg viewBox="0 0 120 120" class="lh-gauge">
<circle class="lh-gauge-base" r="56" cx="60" cy="60" stroke-width="8"></circle>
<circle class="lh-gauge-arc" r="56" cx="60" cy="60" stroke-width="8" style="transform: rotate(-87.95372216024705deg); stroke-dasharray: 351.85837720205683px, 351.85837720205683px;"></circle>
</svg>
</div>
<div class="lh-gauge__percentage">100</div>
<!-- TODO: should likely be an h2 -->
<div class="lh-gauge__label">Performance</div>
</a>
<a href="#accessibility" class="lh-gauge__wrapper lh-gauge__wrapper--pass">
<!-- Wrapper exists for the ::before plugin icon. Cannot create pseudo-elements on svgs. -->
<div class="lh-gauge__svg-wrapper">
<svg viewBox="0 0 120 120" class="lh-gauge">
<circle class="lh-gauge-base" r="56" cx="60" cy="60" stroke-width="8"></circle>
<circle class="lh-gauge-arc" r="56" cx="60" cy="60" stroke-width="8" style="transform: rotate(-87.95372216024705deg); stroke-dasharray: 351.85837720205683px, 351.85837720205683px;"></circle>
</svg>
</div>
<div class="lh-gauge__percentage">100</div>
<!-- TODO: should likely be an h2 -->
<div class="lh-gauge__label">Accessibility</div>
</a>
<a href="#best-practices" class="lh-gauge__wrapper lh-gauge__wrapper--pass">
<!-- Wrapper exists for the ::before plugin icon. Cannot create pseudo-elements on svgs. -->
<div class="lh-gauge__svg-wrapper">
<svg viewBox="0 0 120 120" class="lh-gauge">
<circle class="lh-gauge-base" r="56" cx="60" cy="60" stroke-width="8"></circle>
<circle class="lh-gauge-arc" r="56" cx="60" cy="60" stroke-width="8" style="transform: rotate(-87.95372216024705deg); stroke-dasharray: 351.85837720205683px, 351.85837720205683px;"></circle>
</svg>
</div>
<div class="lh-gauge__percentage">100</div>
<!-- TODO: should likely be an h2 -->
<div class="lh-gauge__label">Best Practices</div>
</a>
<a href="#seo" class="lh-gauge__wrapper lh-gauge__wrapper--pass">
<!-- Wrapper exists for the ::before plugin icon. Cannot create pseudo-elements on svgs. -->
<div class="lh-gauge__svg-wrapper">
<svg viewBox="0 0 120 120" class="lh-gauge">
<circle class="lh-gauge-base" r="56" cx="60" cy="60" stroke-width="8"></circle>
<circle class="lh-gauge-arc" r="56" cx="60" cy="60" stroke-width="8" style="transform: rotate(-87.95372216024705deg); stroke-dasharray: 351.85837720205683px, 351.85837720205683px;"></circle>
</svg>
</div>
<div class="lh-gauge__percentage">100</div>
<!-- TODO: should likely be an h2 -->
<div class="lh-gauge__label">SEO</div>
</a>
<a href="#pwa" class="lh-gauge__wrapper lh-gauge--pwa__wrapper lh-badged--pwa-installable" title="Installable: 1/1, PWA Optimized: 6/8">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" class="lh-gauge lh-gauge--pwa">
<defs>
<linearGradient id="lh-gauge--pwa__check-circle__gradient-1" x1="50%" y1="0%" x2="50%" y2="100%">
<stop stop-color="#00C852" offset="0%"></stop>
<stop stop-color="#009688" offset="100%"></stop>
</linearGradient>
<linearGradient id="lh-gauge--pwa__installable__shadow-gradient-1" x1="76.056%" x2="24.111%" y1="82.995%" y2="24.735%">
<stop stop-color="#A5D6A7" offset="0%"></stop>
<stop stop-color="#80CBC4" offset="100%"></stop>
</linearGradient>
<g id="lh-gauge--pwa__installable-badge-1">
<circle fill="#FFFFFF" cx="10" cy="10" r="10"></circle>
<path fill="#009688" d="M10 4.167A5.835 5.835 0 0 0 4.167 10 5.835 5.835 0 0 0 10 15.833 5.835 5.835 0 0 0 15.833 10 5.835 5.835 0 0 0 10 4.167zm2.917 6.416h-2.334v2.334H9.417v-2.334H7.083V9.417h2.334V7.083h1.166v2.334h2.334v1.166z"></path>
</g>
</defs>
<g stroke="none" fill-rule="nonzero">
<!-- Background and PWA logo (color by default) -->
<circle class="lh-gauge--pwa__disc" cx="30" cy="30" r="30"></circle>
<g class="lh-gauge--pwa__logo">
<path class="lh-gauge--pwa__logo--secondary-color" d="M35.66 19.39l.7-1.75h2L37.4 15 38.6 12l3.4 9h-2.51l-.58-1.61z"></path>
<path class="lh-gauge--pwa__logo--primary-color" d="M33.52 21l3.65-9h-2.42l-2.5 5.82L30.5 12h-1.86l-1.9 5.82-1.35-2.65-1.21 3.72L25.4 21h2.38l1.72-5.2 1.64 5.2z"></path>
<path class="lh-gauge--pwa__logo--secondary-color" fill-rule="nonzero" d="M20.3 17.91h1.48c.45 0 .85-.05 1.2-.15l.39-1.18 1.07-3.3a2.64 2.64 0 0 0-.28-.37c-.55-.6-1.36-.91-2.42-.91H18v9h2.3V17.9zm1.96-3.84c.22.22.33.5.33.87 0 .36-.1.65-.29.87-.2.23-.59.35-1.15.35h-.86v-2.41h.87c.52 0 .89.1 1.1.32z"></path>
</g>
<!-- No badges. -->
<rect class="lh-gauge--pwa__component lh-gauge--pwa__na-line" fill="#FFFFFF" x="20" y="32" width="20" height="4" rx="2"></rect>
<!-- Just installable. -->
<g class="lh-gauge--pwa__component lh-gauge--pwa__installable-badge" transform="translate(20, 29)">
<path fill="url(#lh-gauge--pwa__installable__shadow-gradient-1)" d="M33.629 19.487c-4.272 5.453-10.391 9.39-17.415 10.869L3 17.142 17.142 3 33.63 19.487z"></path>
<use href="#lh-gauge--pwa__installable-badge-1"></use>
</g>
<!-- Full PWA. -->
<g class="lh-gauge--pwa__component lh-gauge--pwa__check-circle" transform="translate(18, 28)">
<circle fill="#FFFFFF" cx="12" cy="12" r="12"></circle>
<path fill="url(#lh-gauge--pwa__check-circle__gradient-1)" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"></path>
</g>
</g>
</svg>
<div class="lh-gauge__label">Progressive Web App</div>
</a>
<div class="lh-highlighter" style="transform: translate(0px);"></div></div><div>
<style>
/* CSS Fireworks. Originally by Eddie Lin
https://codepen.io/paulirish/pen/yEVMbP
*/
.pyro {
display: none;
z-index: 1;
pointer-events: none;
}
.score100 .pyro {
display: block;
}
.score100 .lh-lighthouse stop:first-child {
stop-color: hsla(200, 12%, 95%, 0);
}
.score100 .lh-lighthouse stop:last-child {
stop-color: hsla(65, 81%, 76%, 1);
}
.pyro > .before, .pyro > .after {
position: absolute;
width: 5px;
height: 5px;
border-radius: 2.5px;
box-shadow: 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff, 0 0 #fff;
animation: 1s bang ease-out infinite backwards, 1s gravity ease-in infinite backwards, 5s position linear infinite backwards;
animation-delay: 1s, 1s, 1s;
}
.pyro > .after {
animation-delay: 2.25s, 2.25s, 2.25s;
animation-duration: 1.25s, 1.25s, 6.25s;
}
.fireworks-paused .pyro > div {
animation-play-state: paused;
}
@keyframes bang {
to {
box-shadow: -70px -115.67px #47ebbc, -28px -99.67px #eb47a4, 58px -31.67px #7eeb47, 13px -141.67px #eb47c5, -19px 6.33px #7347eb, -2px -74.67px #ebd247, 24px -151.67px #eb47e0, 57px -138.67px #b4eb47, -51px -104.67px #479eeb, 62px 8.33px #ebcf47, -93px 0.33px #d547eb, -16px -118.67px #47bfeb, 53px -84.67px #47eb83, 66px -57.67px #eb47bf, -93px -65.67px #91eb47, 30px -13.67px #86eb47, -2px -59.67px #83eb47, -44px 1.33px #eb47eb, 61px -58.67px #47eb73, 5px -22.67px #47e8eb, -66px -28.67px #ebe247, 42px -123.67px #eb5547, -75px 26.33px #7beb47, 15px -52.67px #a147eb, 36px -51.67px #eb8347, -38px -12.67px #eb5547, -46px -59.67px #47eb81, 78px -114.67px #eb47ba, 15px -156.67px #eb47bf, -36px 1.33px #eb4783, -72px -86.67px #eba147, 31px -46.67px #ebe247, -68px 29.33px #47e2eb, -55px 19.33px #ebe047, -56px 27.33px #4776eb, -13px -91.67px #eb5547, -47px -138.67px #47ebc7, -18px -96.67px #eb47ac, 11px -88.67px #4783eb, -67px -28.67px #47baeb, 53px 10.33px #ba47eb, 11px 19.33px #5247eb, -5px -11.67px #eb4791, -68px -4.67px #47eba7, 95px -37.67px #eb478b, -67px -162.67px #eb5d47, -54px -120.67px #eb6847, 49px -12.67px #ebe047, 88px 8.33px #47ebda, 97px 33.33px #eb8147, 6px -71.67px #ebbc47;
}
}
@keyframes gravity {
to {
transform: translateY(80px);
opacity: 0;
}
}
@keyframes position {
0%, 19.9% {
margin-top: 4%;
margin-left: 47%;
}
20%, 39.9% {
margin-top: 7%;
margin-left: 30%;
}
40%, 59.9% {
margin-top: 6%;
margin-left: 70%;
}
60%, 79.9% {
margin-top: 3%;
margin-left: 20%;
}
80%, 99.9% {
margin-top: 3%;
margin-left: 80%;
}
}
</style>
<div class="lh-header-container">
<style>
.lh-scores-container {
display: flex;
flex-direction: column;
padding: var(--scores-container-padding);
position: relative;
width: 100%;
}
.lh-sticky-header {
--gauge-circle-size: 36px;
--plugin-badge-size: 18px;
--plugin-icon-size: 75%;
--gauge-wrapper-width: 60px;
--gauge-percentage-font-size: 13px;
position: fixed;
left: 0;
right: 0;
top: var(--topbar-height);
font-weight: 700;
display: none;
justify-content: center;
background-color: var(--sticky-header-background-color);
border-bottom: 1px solid var(--color-gray-200);
padding-top: var(--score-container-padding);
padding-bottom: 4px;
z-index: 1;
pointer-events: none;
}
.lh-devtools .lh-sticky-header {
/* The report within DevTools is placed in a container with overflow, which changes the placement of this header unless we change `position` to `sticky.` */
position: sticky;
}
.lh-sticky-header--visible {
display: grid;
grid-auto-flow: column;
pointer-events: auto;
}
/* Disable the gauge arc animation for the sticky header, so toggling display: none
does not play the animation. */
.lh-sticky-header .lh-gauge-arc {
animation: none;
}
.lh-sticky-header .lh-gauge__label {
display: none;
}
.lh-highlighter {
width: var(--gauge-wrapper-width);
height: 1px;
background-color: var(--highlighter-background-color);
/* Position at bottom of first gauge in sticky header. */
position: absolute;
grid-column: 1;
bottom: -1px;
}
.lh-gauge__wrapper:first-of-type {
contain: none;
}
</style>
<div class="lh-scores-wrapper">
<div class="lh-scores-container score100">
<div class="pyro">
<div class="before"></div>
<div class="after"></div>
</div>
<div class="lh-scores-header">
<a href="#performance" class="lh-gauge__wrapper lh-gauge__wrapper--pass">
<!-- Wrapper exists for the ::before plugin icon. Cannot create pseudo-elements on svgs. -->
<div class="lh-gauge__svg-wrapper">
<svg viewBox="0 0 120 120" class="lh-gauge">
<circle class="lh-gauge-base" r="56" cx="60" cy="60" stroke-width="8"></circle>
<circle class="lh-gauge-arc" r="56" cx="60" cy="60" stroke-width="8" style="transform: rotate(-87.95372216024705deg); stroke-dasharray: 351.85837720205683px, 351.85837720205683px;"></circle>
</svg>
</div>
<div class="lh-gauge__percentage">100</div>
<!-- TODO: should likely be an h2 -->
<div class="lh-gauge__label">Performance</div>
</a>
<a href="#accessibility" class="lh-gauge__wrapper lh-gauge__wrapper--pass">
<!-- Wrapper exists for the ::before plugin icon. Cannot create pseudo-elements on svgs. -->
<div class="lh-gauge__svg-wrapper">
<svg viewBox="0 0 120 120" class="lh-gauge">
<circle class="lh-gauge-base" r="56" cx="60" cy="60" stroke-width="8"></circle>
<circle class="lh-gauge-arc" r="56" cx="60" cy="60" stroke-width="8" style="transform: rotate(-87.95372216024705deg); stroke-dasharray: 351.85837720205683px, 351.85837720205683px;"></circle>
</svg>
</div>
<div class="lh-gauge__percentage">100</div>
<!-- TODO: should likely be an h2 -->
<div class="lh-gauge__label">Accessibility</div>
</a>
<a href="#best-practices" class="lh-gauge__wrapper lh-gauge__wrapper--pass">
<!-- Wrapper exists for the ::before plugin icon. Cannot create pseudo-elements on svgs. -->
<div class="lh-gauge__svg-wrapper">
<svg viewBox="0 0 120 120" class="lh-gauge">
<circle class="lh-gauge-base" r="56" cx="60" cy="60" stroke-width="8"></circle>
<circle class="lh-gauge-arc" r="56" cx="60" cy="60" stroke-width="8" style="transform: rotate(-87.95372216024705deg); stroke-dasharray: 351.85837720205683px, 351.85837720205683px;"></circle>
</svg>
</div>
<div class="lh-gauge__percentage">100</div>
<!-- TODO: should likely be an h2 -->
<div class="lh-gauge__label">Best Practices</div>
</a>
<a href="#seo" class="lh-gauge__wrapper lh-gauge__wrapper--pass">
<!-- Wrapper exists for the ::before plugin icon. Cannot create pseudo-elements on svgs. -->
<div class="lh-gauge__svg-wrapper">
<svg viewBox="0 0 120 120" class="lh-gauge">
<circle class="lh-gauge-base" r="56" cx="60" cy="60" stroke-width="8"></circle>
<circle class="lh-gauge-arc" r="56" cx="60" cy="60" stroke-width="8" style="transform: rotate(-87.95372216024705deg); stroke-dasharray: 351.85837720205683px, 351.85837720205683px;"></circle>
</svg>
</div>
<div class="lh-gauge__percentage">100</div>
<!-- TODO: should likely be an h2 -->
<div class="lh-gauge__label">SEO</div>
</a>
<style>
.lh-gauge--pwa .lh-gauge--pwa__component {
display: none;
}
.lh-gauge--pwa__wrapper:not(.lh-badged--all) .lh-gauge--pwa__logo > path {
/* Gray logo unless everything is passing. */
fill: #B0B0B0;
}
.lh-gauge--pwa__disc {
fill: var(--color-gray-200);
}
.lh-gauge--pwa__logo--primary-color {
fill: #304FFE;
}
.lh-gauge--pwa__logo--secondary-color {
fill: #3D3D3D;
}
.dark .lh-gauge--pwa__logo--secondary-color {
fill: #D8B6B6;
}
/* No passing groups. */
.lh-gauge--pwa__wrapper:not([class*='lh-badged--']) .lh-gauge--pwa__na-line {
display: inline;
}
/* Just optimized. Same n/a line as no passing groups. */
.lh-gauge--pwa__wrapper.lh-badged--pwa-optimized:not(.lh-badged--pwa-installable) .lh-gauge--pwa__na-line {
display: inline;
}
/* Just installable. */
.lh-gauge--pwa__wrapper.lh-badged--pwa-installable .lh-gauge--pwa__installable-badge {
display: inline;
}
/* All passing groups. */
.lh-gauge--pwa__wrapper.lh-badged--all .lh-gauge--pwa__check-circle {
display: inline;
}
</style>
<a href="#pwa" class="lh-gauge__wrapper lh-gauge--pwa__wrapper lh-badged--pwa-installable" title="Installable: 1/1, PWA Optimized: 6/8">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" class="lh-gauge lh-gauge--pwa">
<defs>
<linearGradient id="lh-gauge--pwa__check-circle__gradient-0" x1="50%" y1="0%" x2="50%" y2="100%">
<stop stop-color="#00C852" offset="0%"></stop>
<stop stop-color="#009688" offset="100%"></stop>
</linearGradient>
<linearGradient id="lh-gauge--pwa__installable__shadow-gradient-0" x1="76.056%" x2="24.111%" y1="82.995%" y2="24.735%">
<stop stop-color="#A5D6A7" offset="0%"></stop>
<stop stop-color="#80CBC4" offset="100%"></stop>
</linearGradient>
<g id="lh-gauge--pwa__installable-badge-0">
<circle fill="#FFFFFF" cx="10" cy="10" r="10"></circle>
<path fill="#009688" d="M10 4.167A5.835 5.835 0 0 0 4.167 10 5.835 5.835 0 0 0 10 15.833 5.835 5.835 0 0 0 15.833 10 5.835 5.835 0 0 0 10 4.167zm2.917 6.416h-2.334v2.334H9.417v-2.334H7.083V9.417h2.334V7.083h1.166v2.334h2.334v1.166z"></path>
</g>
</defs>
<g stroke="none" fill-rule="nonzero">
<!-- Background and PWA logo (color by default) -->
<circle class="lh-gauge--pwa__disc" cx="30" cy="30" r="30"></circle>
<g class="lh-gauge--pwa__logo">
<path class="lh-gauge--pwa__logo--secondary-color" d="M35.66 19.39l.7-1.75h2L37.4 15 38.6 12l3.4 9h-2.51l-.58-1.61z"></path>
<path class="lh-gauge--pwa__logo--primary-color" d="M33.52 21l3.65-9h-2.42l-2.5 5.82L30.5 12h-1.86l-1.9 5.82-1.35-2.65-1.21 3.72L25.4 21h2.38l1.72-5.2 1.64 5.2z"></path>
<path class="lh-gauge--pwa__logo--secondary-color" fill-rule="nonzero" d="M20.3 17.91h1.48c.45 0 .85-.05 1.2-.15l.39-1.18 1.07-3.3a2.64 2.64 0 0 0-.28-.37c-.55-.6-1.36-.91-2.42-.91H18v9h2.3V17.9zm1.96-3.84c.22.22.33.5.33.87 0 .36-.1.65-.29.87-.2.23-.59.35-1.15.35h-.86v-2.41h.87c.52 0 .89.1 1.1.32z"></path>
</g>
<!-- No badges. -->
<rect class="lh-gauge--pwa__component lh-gauge--pwa__na-line" fill="#FFFFFF" x="20" y="32" width="20" height="4" rx="2"></rect>
<!-- Just installable. -->
<g class="lh-gauge--pwa__component lh-gauge--pwa__installable-badge" transform="translate(20, 29)">
<path fill="url(#lh-gauge--pwa__installable__shadow-gradient-0)" d="M33.629 19.487c-4.272 5.453-10.391 9.39-17.415 10.869L3 17.142 17.142 3 33.63 19.487z"></path>
<use href="#lh-gauge--pwa__installable-badge-0"></use>
</g>
<!-- Full PWA. -->
<g class="lh-gauge--pwa__component lh-gauge--pwa__check-circle" transform="translate(18, 28)">
<circle fill="#FFFFFF" cx="12" cy="12" r="12"></circle>
<path fill="url(#lh-gauge--pwa__check-circle__gradient-0)" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"></path>
</g>
</g>
</svg>
<div class="lh-gauge__label">Progressive Web App</div>
</a>
</div>
<div class="lh-scorescale">
<span class="lh-scorescale-range lh-scorescale-range--fail">049</span>
<span class="lh-scorescale-range lh-scorescale-range--average">5089</span>
<span class="lh-scorescale-range lh-scorescale-range--pass">90100</span>
</div>
</div>
</div>
</div>
</div><div class="lh-report"><div></div><div class="lh-categories"><div class="lh-category-wrapper"><div class="lh-category"><span class="lh-permalink" id="performance"></span>
<div class="lh-category-header">
<div class="lh-score__gauge" role="heading" aria-level="2">
<a href="#performance" class="lh-gauge__wrapper lh-gauge__wrapper--pass">
<!-- Wrapper exists for the ::before plugin icon. Cannot create pseudo-elements on svgs. -->
<div class="lh-gauge__svg-wrapper">
<svg viewBox="0 0 120 120" class="lh-gauge">
<circle class="lh-gauge-base" r="56" cx="60" cy="60" stroke-width="8"></circle>
<circle class="lh-gauge-arc" r="56" cx="60" cy="60" stroke-width="8" style="transform: rotate(-87.95372216024705deg); stroke-dasharray: 351.85837720205683px, 351.85837720205683px;"></circle>
</svg>
</div>
<div class="lh-gauge__percentage">100</div>
<!-- TODO: should likely be an h2 -->
<div class="lh-gauge__label">Performance</div>
</a>
</div>
<div class="lh-category-header__description"></div>
</div>
<div class="lh-audit-group lh-audit-group--metrics"><div class="lh-audit-group__header"><span class="lh-audit-group__title">Metrics</span></div>
<input class="lh-metrics-toggle__input" type="checkbox" id="toggle-metric-descriptions" aria-label="Toggle the display of metric descriptions">
<label class="lh-metrics-toggle__label" for="toggle-metric-descriptions">
<div class="lh-metrics-toggle__icon lh-metrics-toggle__icon--less" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">
<path class="lh-metrics-toggle__lines" d="M4 9h16v2H4zm0 4h10v2H4z"></path>
</svg>
</div>
<div class="lh-metrics-toggle__icon lh-metrics-toggle__icon--more" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path class="lh-metrics-toggle__lines" d="M3 18h12v-2H3v2zM3 6v2h18V6H3zm0 7h18v-2H3v2z"></path>
</svg>
</div>
</label>
<div class="lh-metrics-container"><div class="lh-metric lh-metric--pass" id="first-contentful-paint">
<div class="lh-metric__innerwrap">
<span class="lh-metric__title">First Contentful Paint</span>
<div class="lh-metric__value">1.4&nbsp;s</div>
<div class="lh-metric__description"><span>First Contentful Paint marks the time at which the first text or image is painted. <a rel="noopener" target="_blank" href="https://web.dev/first-contentful-paint/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
</div>
</div><div class="lh-metric lh-metric--pass" id="speed-index">
<div class="lh-metric__innerwrap">
<span class="lh-metric__title">Speed Index</span>
<div class="lh-metric__value">1.4&nbsp;s</div>
<div class="lh-metric__description"><span>Speed Index shows how quickly the contents of a page are visibly populated. <a rel="noopener" target="_blank" href="https://web.dev/speed-index/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
</div>
</div><div class="lh-metric lh-metric--pass" id="largest-contentful-paint">
<div class="lh-metric__innerwrap">
<span class="lh-metric__title">Largest Contentful Paint</span>
<div class="lh-metric__value">1.5&nbsp;s</div>
<div class="lh-metric__description"><span>Largest Contentful Paint marks the time at which the largest text or image is painted. <a rel="noopener" target="_blank" href="https://web.dev/lighthouse-largest-contentful-paint/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a></span></div>
</div>
</div><div class="lh-metric lh-metric--pass" id="interactive">
<div class="lh-metric__innerwrap">
<span class="lh-metric__title">Time to Interactive</span>
<div class="lh-metric__value">1.4&nbsp;s</div>
<div class="lh-metric__description"><span>Time to interactive is the amount of time it takes for the page to become fully interactive. <a rel="noopener" target="_blank" href="https://web.dev/interactive/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
</div>
</div><div class="lh-metric lh-metric--pass" id="total-blocking-time">
<div class="lh-metric__innerwrap">
<span class="lh-metric__title">Total Blocking Time</span>
<div class="lh-metric__value">10&nbsp;ms</div>
<div class="lh-metric__description"><span>Sum of all time periods between FCP and Time to Interactive, when task length exceeded 50ms, expressed in milliseconds. <a rel="noopener" target="_blank" href="https://web.dev/lighthouse-total-blocking-time/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
</div>
</div><div class="lh-metric lh-metric--pass" id="cumulative-layout-shift">
<div class="lh-metric__innerwrap">
<span class="lh-metric__title">Cumulative Layout Shift</span>
<div class="lh-metric__value">0</div>
<div class="lh-metric__description"><span>Cumulative Layout Shift measures the movement of visible elements within the viewport. <a rel="noopener" target="_blank" href="https://web.dev/cls/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
</div>
</div></div><div class="lh-metrics__disclaimer"><span>Values are estimated and may vary. The <a rel="noopener" target="_blank" href="https://web.dev/performance-scoring/?utm_source=lighthouse&amp;utm_medium=cli">performance score is calculated</a> directly from these metrics.</span><a class="lh-calclink" target="_blank" href="https://googlechrome.github.io/lighthouse/scorecalc/#FCP=1369&amp;SI=1369&amp;LCP=1500&amp;TTI=1400&amp;TBT=6&amp;CLS=0&amp;FMP=1369&amp;device=mobile&amp;version=8.0.0">See calculator.</a></div><div class="lh-buttons"><button class="lh-button report-icon report-icon--treemap">View Treemap</button></div></div><div class="lh-filmstrip-container" id="screenshot-thumbnails"><div class="lh-filmstrip"><div class="lh-filmstrip__frame"><img class="lh-filmstrip__thumbnail" src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIANUAeAMBEQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD//Z" alt="Screenshot"></div><div class="lh-filmstrip__frame"><img class="lh-filmstrip__thumbnail" src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRQBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIANUAeAMBEQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKACgAoAKACgAoAKACgAoAKACgAoAKAM3X7mW0sI5IXMbm5t0JA/haZFYfiCR+NAGg+Qp2jJxwKAPF9b+LHjLwpHq12nh668ZqLy9ittN0/w/e6dJbRwx6g8W6ZzKtx5r2kESuiouZlk+7LEpAOc1r9rPVfCvibw5ouv/D+40i61/UlsLFZryQGUNc6dFuG63Ubgl9M5BIUG1Kb8uCAD2PwB4v1Dxhb6tLf6FPoi2d79ltzOJVF3H5Ub+colijcLukZMFB80bYJGDQB1VABQAUAFABQAUAFABQB5b8VPiB4q8E6zaTaLpja/p4tppJtKt9Eu5p5XSC4kB+2RMyQjdFGuwxSO27ChmdRQBzev/tB+MNHg8USwfCbWr59IuZ4rS0jF0Z9UijNuEnhK2rRbXE0xCtIJP3GAjEtsAPXvEjbtKhOME3dpx/28R0AbFACbQOw9aAEdFkXayhh6EZFACgYzQAtABQAUAFABQAUAITgZoA860D49eFteutYi3XdhHpusHQ2ubqEeTNc+c8G1XQsFPmxlNsmxwWiJUCaIuCuRv8AtF/DSJJpG8X6
<div class="lh-load-opportunity__col lh-load-opportunity__col--one">Opportunity</div>
<div class="lh-load-opportunity__col lh-load-opportunity__col--two">Estimated Savings</div>
</div><div class="lh-audit lh-audit--load-opportunity lh-audit--numeric lh-audit--average" id="unused-css-rules">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<div class="lh-load-opportunity__cols">
<div class="lh-load-opportunity__col lh-load-opportunity__col--one">
<span class="lh-audit__score-icon"></span>
<div class="lh-audit__title"><span>Reduce unused CSS</span></div>
</div>
<div class="lh-load-opportunity__col lh-load-opportunity__col--two">
<div class="lh-load-opportunity__sparkline" title="Potential savings of 28&nbsp;KiB">
<div class="lh-sparkline"><div class="lh-sparkline__bar" style="width: 14.499999999999998%;"></div></div>
</div>
<div class="lh-audit__display-text" title="Potential savings of 28&nbsp;KiB">0.29&nbsp;s</div>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</div>
</div>
</summary>
<div class="lh-audit__description"><span>Reduce unused rules from stylesheets and defer CSS not used for above-the-fold content to decrease bytes consumed by network activity. <a rel="noopener" target="_blank" href="https://web.dev/unused-css-rules/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span><span class="lh-audit__adorn" title="Relevant to First Contentful Paint">FCP</span><span class="lh-audit__adorn" title="Relevant to Largest Contentful Paint">LCP</span></div>
<div class="lh-audit__stackpacks"></div>
<style>
.lh-3p-filter {
background-color: var(--table-higlight-background-color);
color: var(--color-gray-600);
float: right;
padding: 6px;
}
.lh-3p-filter-label, .lh-3p-filter-input {
vertical-align: middle;
user-select: none;
}
.lh-3p-filter-input:disabled + .lh-3p-ui-string {
text-decoration: line-through;
}
</style>
<div class="lh-3p-filter">
<label class="lh-3p-filter-label" for="lh-3p-filter-label--0">
<input type="checkbox" class="lh-3p-filter-input" checked="" id="lh-3p-filter-label--0" disabled="">
<span class="lh-3p-ui-string">Show 3rd-party resources</span> (<span class="lh-3p-filter-count">0</span>)
</label>
</div>
<table class="lh-table lh-details"><thead><tr><th class="lh-table-column--url"><div class="lh-text">URL</div></th><th class="lh-table-column--bytes"><div class="lh-text">Transfer Size</div></th><th class="lh-table-column--bytes"><div class="lh-text">Potential Savings</div></th></tr></thead><tbody><tr class="lh-row--even"><td class="lh-table-column--url"><div class="lh-text__url" title="http://localhost:8008/congo/css/main.bundle.min.6783e342ffedbcc698d9afb9ffe11276f4f9de79bfc45cf670b35d5f9948d266086106596bc9e59820d7de0959e5889ce97e073fbafd380be9dc7b33b75295f2.css" data-url="http://localhost:8008/congo/css/main.bundle.min.6783e342ffedbcc698d9afb9ffe11276f4f9de79bfc45cf670b35d5f9948d266086106596bc9e59820d7de0959e5889ce97e073fbafd380be9dc7b33b75295f2.css"><a rel="noopener" target="_blank" href="http://localhost:8008/congo/css/main.bundle.min.6783e342ffedbcc698d9afb9ffe11276f4f9de79bfc45cf670b35d5f9948d266086106596bc9e59820d7de0959e5889ce97e073fbafd380be9dc7b33b75295f2.css" class="lh-link">…css/main.bundle.min.6783e34….css</a><div class="lh-text lh-text__url-host">(localhost)</div></div></td><td class="lh-table-column--bytes"><div class="lh-text" title="40,665&nbsp;bytes">39.7&nbsp;KiB</div></td><td class="lh-table-column--bytes"><div class="lh-text" title="28,581&nbsp;bytes">27.9&nbsp;KiB</div></td></tr></tbody></table></details>
</div><div class="lh-audit lh-audit--load-opportunity lh-audit--numeric lh-audit--average" id="unused-javascript">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<div class="lh-load-opportunity__cols">
<div class="lh-load-opportunity__col lh-load-opportunity__col--one">
<span class="lh-audit__score-icon"></span>
<div class="lh-audit__title"><span>Reduce unused JavaScript</span></div>
</div>
<div class="lh-load-opportunity__col lh-load-opportunity__col--two">
<div class="lh-load-opportunity__sparkline" title="Potential savings of 21&nbsp;KiB">
<div class="lh-sparkline"><div class="lh-sparkline__bar" style="width: 7.5%;"></div></div>
</div>
<div class="lh-audit__display-text" title="Potential savings of 21&nbsp;KiB">0.15&nbsp;s</div>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</div>
</div>
</summary>
<div class="lh-audit__description"><span>Reduce unused JavaScript and defer loading scripts until they are required to decrease bytes consumed by network activity. <a rel="noopener" target="_blank" href="https://web.dev/unused-javascript/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span><span class="lh-audit__adorn" title="Relevant to Largest Contentful Paint">LCP</span></div>
<div class="lh-audit__stackpacks"></div>
<div class="lh-3p-filter">
<label class="lh-3p-filter-label" for="lh-3p-filter-label--1">
<input type="checkbox" class="lh-3p-filter-input" checked="" id="lh-3p-filter-label--1" disabled="">
<span class="lh-3p-ui-string">Show 3rd-party resources</span> (<span class="lh-3p-filter-count">0</span>)
</label>
</div>
<table class="lh-table lh-details"><thead><tr><th class="lh-table-column--url"><div class="lh-text">URL</div></th><th class="lh-table-column--bytes"><div class="lh-text">Transfer Size</div></th><th class="lh-table-column--bytes"><div class="lh-text">Potential Savings</div></th></tr></thead><tbody><tr class="lh-row--even"><td class="lh-table-column--url"><div class="lh-text__url" title="http://localhost:8008/congo/js/main.bundle.min.81d5d44787f3f7c0220442722b6d0c72da50dab39103321d578502e81b226ed5a87e765e5e2a7c0533fdddeb4697bd499a39be89310453fba5c129f5ba69bdea.js" data-url="http://localhost:8008/congo/js/main.bundle.min.81d5d44787f3f7c0220442722b6d0c72da50dab39103321d578502e81b226ed5a87e765e5e2a7c0533fdddeb4697bd499a39be89310453fba5c129f5ba69bdea.js"><a rel="noopener" target="_blank" href="http://localhost:8008/congo/js/main.bundle.min.81d5d44787f3f7c0220442722b6d0c72da50dab39103321d578502e81b226ed5a87e765e5e2a7c0533fdddeb4697bd499a39be89310453fba5c129f5ba69bdea.js" class="lh-link">…js/main.bundle.min.81d5d44….js</a><div class="lh-text lh-text__url-host">(localhost)</div></div></td><td class="lh-table-column--bytes"><div class="lh-text" title="27,861&nbsp;bytes">27.2&nbsp;KiB</div></td><td class="lh-table-column--bytes"><div class="lh-text" title="21,903&nbsp;bytes">21.4&nbsp;KiB</div></td></tr></tbody></table></details>
</div></div><div class="lh-audit-group lh-audit-group--diagnostics"><div class="lh-audit-group__header"><span class="lh-audit-group__title">Diagnostics</span><span class="lh-audit-group__description">More information about the performance of your application. These numbers don't <a rel="noopener" target="_blank" href="https://web.dev/performance-scoring/?utm_source=lighthouse&amp;utm_medium=cli">directly affect</a> the Performance score.</span></div><div class="lh-audit lh-audit--informative" id="critical-request-chains">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Avoid chaining critical requests</span></span>
<span class="lh-audit__display-text">1 chain found</span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>The Critical Request Chains below show you what resources are loaded 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. <a rel="noopener" target="_blank" href="https://web.dev/critical-request-chains/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span><span class="lh-audit__adorn" title="Relevant to First Contentful Paint">FCP</span><span class="lh-audit__adorn" title="Relevant to Largest Contentful Paint">LCP</span></div>
<div class="lh-audit__stackpacks"></div>
<div class="lh-crc-container lh-details">
<style>
.lh-crc .tree-marker {
width: 12px;
height: 26px;
display: block;
float: left;
background-position: top left;
}
.lh-crc .horiz-down {
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>');
}
.lh-crc .right {
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>');
}
.lh-crc .up-right {
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>');
}
.lh-crc .vert-right {
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>');
}
.lh-crc .vert {
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>');
}
.lh-crc .crc-tree {
font-size: 14px;
width: 100%;
overflow-x: auto;
}
.lh-crc .crc-node {
height: 26px;
line-height: 26px;
white-space: nowrap;
}
.lh-crc .crc-node__tree-value {
margin-left: 10px;
}
.lh-crc .crc-node__tree-value div {
display: inline;
}
.lh-crc .crc-node__chain-duration {
font-weight: 700;
}
.lh-crc .crc-initial-nav {
color: #595959;
font-style: italic;
}
.lh-crc__summary-value {
margin-bottom: 10px;
}
</style>
<div>
<div class="lh-crc__summary-value">
<span class="lh-crc__longest_duration_label">Maximum critical path latency:</span> <b class="lh-crc__longest_duration">20&nbsp;ms</b>
</div>
</div>
<div class="lh-crc">
<div class="crc-initial-nav">Initial Navigation</div>
<!-- stamp for each chain -->
<template id="tmpl-lh-crc__chains">
<div class="crc-node">
<span class="crc-node__tree-marker">
</span>
<span class="crc-node__tree-value">
</span>
</div>
</template>
<div class="crc-node" title="http://localhost:8008/congo/samples/emoji/">
<span class="crc-node__tree-marker">
<span class="tree-marker up-right"></span><span class="tree-marker right"></span><span class="tree-marker horiz-down"></span></span>
<span class="crc-node__tree-value">
<div class="lh-text__url" title="http://localhost:8008/congo/samples/emoji/" data-url="http://localhost:8008/congo/samples/emoji/"><a rel="noopener" target="_blank" href="http://localhost:8008/congo/samples/emoji/" class="lh-link">…samples/emoji</a><div class="lh-text lh-text__url-host">(localhost)</div></div></span>
</div>
<div class="crc-node" title="http://localhost:8008/congo/css/main.bundle.min.6783e342ffedbcc698d9afb9ffe11276f4f9de79bfc45cf670b35d5f9948d266086106596bc9e59820d7de0959e5889ce97e073fbafd380be9dc7b33b75295f2.css">
<span class="crc-node__tree-marker">
<span class="tree-marker"></span><span class="tree-marker"></span><span class="tree-marker up-right"></span><span class="tree-marker right"></span><span class="tree-marker right"></span></span>
<span class="crc-node__tree-value">
<div class="lh-text__url" title="http://localhost:8008/congo/css/main.bundle.min.6783e342ffedbcc698d9afb9ffe11276f4f9de79bfc45cf670b35d5f9948d266086106596bc9e59820d7de0959e5889ce97e073fbafd380be9dc7b33b75295f2.css" data-url="http://localhost:8008/congo/css/main.bundle.min.6783e342ffedbcc698d9afb9ffe11276f4f9de79bfc45cf670b35d5f9948d266086106596bc9e59820d7de0959e5889ce97e073fbafd380be9dc7b33b75295f2.css"><a rel="noopener" target="_blank" href="http://localhost:8008/congo/css/main.bundle.min.6783e342ffedbcc698d9afb9ffe11276f4f9de79bfc45cf670b35d5f9948d266086106596bc9e59820d7de0959e5889ce97e073fbafd380be9dc7b33b75295f2.css" class="lh-link">…css/main.bundle.min.6783e34….css</a><div class="lh-text lh-text__url-host">(localhost)</div></div><span class="crc-node__chain-duration"> - 0&nbsp;ms, </span><span class="crc-node__chain-duration">39.71&nbsp;KiB</span></span>
</div>
</div>
</div></details>
</div><div class="lh-audit lh-audit--informative" id="resource-summary">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Keep request counts low and transfer sizes small</span></span>
<span class="lh-audit__display-text">4 requests • 92 KiB</span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>To set budgets for the quantity and size of page resources, add a budget.json file. <a rel="noopener" target="_blank" href="https://web.dev/use-lighthouse-for-performance-budgets/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<table class="lh-table lh-details"><thead><tr><th class="lh-table-column--text"><div class="lh-text">Resource Type</div></th><th class="lh-table-column--numeric"><div class="lh-text">Requests</div></th><th class="lh-table-column--bytes"><div class="lh-text">Transfer Size</div></th></tr></thead><tbody><tr class="lh-row--even"><td class="lh-table-column--text"><div class="lh-text">Total</div></td><td class="lh-table-column--numeric"><div class="lh-numeric">4</div></td><td class="lh-table-column--bytes"><div class="lh-text" title="93,852&nbsp;bytes">91.7&nbsp;KiB</div></td></tr><tr class="lh-row--odd"><td class="lh-table-column--text"><div class="lh-text">Stylesheet</div></td><td class="lh-table-column--numeric"><div class="lh-numeric">1</div></td><td class="lh-table-column--bytes"><div class="lh-text" title="40,665&nbsp;bytes">39.7&nbsp;KiB</div></td></tr><tr class="lh-row--even"><td class="lh-table-column--text"><div class="lh-text">Script</div></td><td class="lh-table-column--numeric"><div class="lh-numeric">1</div></td><td class="lh-table-column--bytes"><div class="lh-text" title="27,861&nbsp;bytes">27.2&nbsp;KiB</div></td></tr><tr class="lh-row--odd"><td class="lh-table-column--text"><div class="lh-text">Document</div></td><td class="lh-table-column--numeric"><div class="lh-numeric">1</div></td><td class="lh-table-column--bytes"><div class="lh-text" title="17,411&nbsp;bytes">17.0&nbsp;KiB</div></td></tr><tr class="lh-row--even"><td class="lh-table-column--text"><div class="lh-text">Image</div></td><td class="lh-table-column--numeric"><div class="lh-numeric">1</div></td><td class="lh-table-column--bytes"><div class="lh-text" title="7,915&nbsp;bytes">7.7&nbsp;KiB</div></td></tr><tr class="lh-row--odd"><td class="lh-table-column--text"><div class="lh-text">Media</div></td><td class="lh-table-column--numeric"><div class="lh-numeric">0</div></td><td class="lh-table-column--bytes"><div class="lh-text" title="0&nbsp;bytes">0.0&nbsp;KiB</div></td></tr><tr class="lh-row--even"><td class="lh-table-column--text"><div class="lh-text">Font</div></td><td class="lh-table-column--numeric"><div class="lh-numeric">0</div></td><td class="lh-table-column--bytes"><div class="lh-text" title="0&nbsp;bytes">0.0&nbsp;KiB</div></td></tr><tr class="lh-row--odd"><td class="lh-table-column--text"><div class="lh-text">Other</div></td><td class="lh-table-column--numeric"><div class="lh-numeric">0</div></td><td class="lh-table-column--bytes"><div class="lh-text" title="0&nbsp;bytes">0.0&nbsp;KiB</div></td></tr><tr class="lh-row--even"><td class="lh-table-column--text"><div class="lh-text">Third-party</div></td><td class="lh-table-column--numeric"><div class="lh-numeric">0</div></td><td class="lh-table-column--bytes"><div class="lh-text" title="0&nbsp;bytes">0.0&nbsp;KiB</div></td></tr></tbody></table></details>
</div><div class="lh-audit lh-audit--informative" id="largest-contentful-paint-element">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Largest Contentful Paint element</span></span>
<span class="lh-audit__display-text">1 element found</span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>This is the largest contentful element painted within the viewport. <a rel="noopener" target="_blank" href="https://web.dev/lighthouse-largest-contentful-paint/?utm_source=lighthouse&amp;utm_medium=cli">Learn More</a></span><span class="lh-audit__adorn" title="Relevant to Largest Contentful Paint">LCP</span></div>
<div class="lh-audit__stackpacks"></div>
<table class="lh-table lh-details"><thead><tr><th class="lh-table-column--node"><div class="lh-text">Element</div></th></tr></thead><tbody><tr class="lh-row--even"><td class="lh-table-column--node"><span class="lh-node" title="section.flex > div.min-w-0 > div.flex > span.dark:text-neutral-300" data-path="1,HTML,1,BODY,2,MAIN,0,ARTICLE,1,SECTION,0,DIV,1,DIV,1,SPAN" data-selector="section.flex > div.min-w-0 > div.flex > span.dark:text-neutral-300" data-snippet="<span class=&quot;dark:text-neutral-300&quot;>"><div>Note: The rendering of these glyphs depends on the browser and the platform. To…</div><div class="lh-node__snippet">&lt;span class="dark:text-neutral-300"&gt;</div></span></td></tr></tbody></table></details>
</div><div class="lh-audit lh-audit--informative" id="long-tasks">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Avoid long main-thread tasks</span></span>
<span class="lh-audit__display-text">2 long tasks found</span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Lists the longest tasks on the main thread, useful for identifying worst contributors to input delay. <a rel="noopener" target="_blank" href="https://web.dev/long-tasks-devtools/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a></span><span class="lh-audit__adorn" title="Relevant to Total Blocking Time">TBT</span></div>
<div class="lh-audit__stackpacks"></div>
<div class="lh-3p-filter">
<label class="lh-3p-filter-label" for="lh-3p-filter-label--2">
<input type="checkbox" class="lh-3p-filter-input" checked="" id="lh-3p-filter-label--2" disabled="">
<span class="lh-3p-ui-string">Show 3rd-party resources</span> (<span class="lh-3p-filter-count">0</span>)
</label>
</div>
<table class="lh-table lh-details"><thead><tr><th class="lh-table-column--url"><div class="lh-text">URL</div></th><th class="lh-table-column--ms"><div class="lh-text">Start Time</div></th><th class="lh-table-column--ms"><div class="lh-text">Duration</div></th></tr></thead><tbody><tr class="lh-row--even"><td class="lh-table-column--url"><div class="lh-text__url" title="http://localhost:8008/congo/samples/emoji/" data-url="http://localhost:8008/congo/samples/emoji/"><a rel="noopener" target="_blank" href="http://localhost:8008/congo/samples/emoji/" class="lh-link">…samples/emoji</a><div class="lh-text lh-text__url-host">(localhost)</div></div></td><td class="lh-table-column--ms"><div class="lh-text">785&nbsp;ms</div></td><td class="lh-table-column--ms"><div class="lh-text">584&nbsp;ms</div></td></tr><tr class="lh-row--odd"><td class="lh-table-column--url"><div class="lh-text__url" title="http://localhost:8008/congo/samples/emoji/" data-url="http://localhost:8008/congo/samples/emoji/"><a rel="noopener" target="_blank" href="http://localhost:8008/congo/samples/emoji/" class="lh-link">…samples/emoji</a><div class="lh-text lh-text__url-host">(localhost)</div></div></td><td class="lh-table-column--ms"><div class="lh-text">1,369&nbsp;ms</div></td><td class="lh-table-column--ms"><div class="lh-text">61&nbsp;ms</div></td></tr></tbody></table></details>
</div><div class="lh-audit lh-audit--informative" id="non-composited-animations">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Avoid non-composited animations</span></span>
<span class="lh-audit__display-text">18 animated elements found</span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Animations which are not composited can be janky and increase CLS. <a rel="noopener" target="_blank" href="https://web.dev/non-composited-animations?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a></span><span class="lh-audit__adorn" title="Relevant to Cumulative Layout Shift">CLS</span></div>
<div class="lh-audit__stackpacks"></div>
<table class="lh-table lh-details"><thead><tr><th class="lh-table-column--node"><div class="lh-text">Element</div></th><th class="lh-table-column--text"><div class="lh-text">Name</div></th></tr></thead><tbody><tr class="lh-row--even"><td class="lh-table-column--node"><span class="lh-node" title="body.flex > div#the-top > a.px-3" data-path="1,HTML,1,BODY,0,DIV,0,A" data-selector="body.flex > div#the-top > a.px-3" data-snippet="<a class=&quot;px-3 py-1 text-sm -translate-y-8 rounded-b-lg bg-primary-200 dark:bg-neutr…&quot; href=&quot;#main-content&quot;>"><div>↓Skip to main content</div><div class="lh-node__snippet">&lt;a class="px-3 py-1 text-sm -translate-y-8 rounded-b-lg bg-primary-200 dark:bg-neutr…" href="#main-content"&gt;</div></span></td><td class="lh-table-column--empty"></td></tr><tr class="lh-sub-item-row lh-row--even"><td class="lh-table-column--text"><div class="lh-text">Unsupported CSS Property: background-color</div></td><td class="lh-table-column--text"><div class="lh-text">background-color</div></td></tr><tr class="lh-sub-item-row lh-row--even"><td class="lh-table-column--text"><div class="lh-text">Unsupported CSS Property: color</div></td><td class="lh-table-column--text"><div class="lh-text">color</div></td></tr><tr class="lh-row--odd"><td class="lh-table-column--node"><span class="lh-node" title="body.flex > header.flex > div > a.hover:underline" data-path="1,HTML,1,BODY,1,HEADER,0,DIV,0,A" data-selector="body.flex > header.flex > div > a.hover:underline" data-snippet="<a class=&quot;hover:underline hover:decoration-primary-500 hover:decoration-2 hover:unde…&quot; rel=&quot;me&quot; href=&quot;/congo/&quot;>"><div>Congo</div><div class="lh-node__snippet">&lt;a class="hover:underline hover:decoration-primary-500 hover:decoration-2 hover:unde…" rel="me" href="/congo/"&gt;</div></span></td><td class="lh-table-column--empty"></td></tr><tr class="lh-sub-item-row lh-row--odd"><td class="lh-table-column--text"><div class="lh-text">Unsupported CSS Property: color</div></td><td class="lh-table-column--text"><div class="lh-text">color</div></td></tr><tr class="lh-row--even"><td class="lh-table-column--node"><span class="lh-node" title="nav > ul.flex > li.mb-1 > a.hover:underline" data-path="1,HTML,1,BODY,1,HEADER,1,NAV,0,UL,0,LI,0,A" data-selector="nav > ul.flex > li.mb-1 > a.hover:underline" data-snippet="<a class=&quot;hover:underline hover:decoration-primary-500 hover:decoration-2 hover:unde…&quot; href=&quot;/congo/docs/&quot;>"><div>Docs</div><div class="lh-node__snippet">&lt;a class="hover:underline hover:decoration-primary-500 hover:decoration-2 hover:unde…" href="/congo/docs/"&gt;</div></span></td><td class="lh-table-column--empty"></td></tr><tr class="lh-sub-item-row lh-row--even"><td class="lh-table-column--text"><div class="lh-text">Unsupported CSS Property: color</div></td><td class="lh-table-column--text"><div class="lh-text">color</div></td></tr><tr class="lh-row--odd"><td class="lh-table-column--node"><span class="lh-node" title="nav > ul.flex > li.mb-1 > a.hover:underline" data-path="1,HTML,1,BODY,1,HEADER,1,NAV,0,UL,1,LI,0,A" data-selector="nav > ul.flex > li.mb-1 > a.hover:underline" data-snippet="<a class=&quot;hover:underline hover:decoration-primary-500 hover:decoration-2 hover:unde…&quot; href=&quot;/congo/samples/&quot;>"><div>Samples</div><div class="lh-node__snippet">&lt;a class="hover:underline hover:decoration-primary-500 hover:decoration-2 hover:unde…" href="/congo/samples/"&gt;</div></span></td><td class="lh-table-column--empty"></td></tr><tr class="lh-sub-item-row lh-row--odd"><td class="lh-table-column--text"><div class="lh-text">Unsupported CSS Property: color</div></td><td class="lh-table-column--text"><div class="lh-text">color</div></td></tr><tr class="lh-row--even"><td class="lh-table-column--node"><span class="lh-node" title="nav > ul.flex > li.mb-1 > a.hover:underline" data-path="1,HTML,1,BODY,1,HEADER,1,NAV,0,UL,2,LI,0,A" data-selector="nav > ul.flex > li.mb-1 > a.hover:underline" data-snippet="<a class=&quot;hover:underline hover:decoration-primary-50
Diagrams and Flowcharts
6 March 2019</div><div class="lh-node__snippet">&lt;a class="flex" href="/congo/samples/diagrams-flowcharts/"&gt;</div></span></td><td class="lh-table-column--empty"></td></tr><tr class="lh-sub-item-row lh-row--even"><td class="lh-table-column--text"><div class="lh-text">Unsupported CSS Property: color</div></td><td class="lh-table-column--text"><div class="lh-text">color</div></td></tr><tr class="lh-row--odd"><td class="lh-table-column--node"><span class="lh-node" title="div#search-wrapper > div#search-modal > header.relative > button#close-search-button" data-path="1,HTML,1,BODY,3,DIV,0,DIV,0,HEADER,1,BUTTON" data-selector="div#search-wrapper > div#search-modal > header.relative > button#close-search-button" data-snippet="<button id=&quot;close-search-button&quot; class=&quot;flex items-center justify-center w-8 h-8 text-neutral-700 dark:text-neutra…&quot;>"><div>button</div><div class="lh-node__snippet">&lt;button id="close-search-button" class="flex items-center justify-center w-8 h-8 text-neutral-700 dark:text-neutra…"&gt;</div></span></td><td class="lh-table-column--empty"></td></tr><tr class="lh-sub-item-row lh-row--odd"><td class="lh-table-column--text"><div class="lh-text">Unsupported CSS Property: color</div></td><td class="lh-table-column--text"><div class="lh-text">color</div></td></tr><tr class="lh-row--even"><td class="lh-table-column--node"><span class="lh-node" title="div.flex > div > p.text-xs > a.hover:underline" data-path="1,HTML,1,BODY,4,FOOTER,0,DIV,0,DIV,1,P,1,A" data-selector="div.flex > div > p.text-xs > a.hover:underline" data-snippet="<a class=&quot;hover:underline hover:decoration-primary-400 hover:text-primary-500&quot; href=&quot;https://gohugo.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"><div>Hugo</div><div class="lh-node__snippet">&lt;a class="hover:underline hover:decoration-primary-400 hover:text-primary-500" href="https://gohugo.io/" target="_blank" rel="noopener noreferrer"&gt;</div></span></td><td class="lh-table-column--empty"></td></tr><tr class="lh-sub-item-row lh-row--even"><td class="lh-table-column--text"><div class="lh-text">Unsupported CSS Property: color</div></td><td class="lh-table-column--text"><div class="lh-text">color</div></td></tr><tr class="lh-row--odd"><td class="lh-table-column--node"><span class="lh-node" title="div.flex > div > p.text-xs > a.hover:underline" data-path="1,HTML,1,BODY,4,FOOTER,0,DIV,0,DIV,1,P,3,A" data-selector="div.flex > div > p.text-xs > a.hover:underline" data-snippet="<a class=&quot;hover:underline hover:decoration-primary-400 hover:text-primary-500&quot; href=&quot;https://git.io/hugo-congo&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;>"><div>Congo</div><div class="lh-node__snippet">&lt;a class="hover:underline hover:decoration-primary-400 hover:text-primary-500" href="https://git.io/hugo-congo" target="_blank" rel="noopener noreferrer"&gt;</div></span></td><td class="lh-table-column--empty"></td></tr><tr class="lh-sub-item-row lh-row--odd"><td class="lh-table-column--text"><div class="lh-text">Unsupported CSS Property: color</div></td><td class="lh-table-column--text"><div class="lh-text">color</div></td></tr></tbody></table></details>
</div></div><details class="lh-clump lh-audit-group lh-clump--passed">
<summary>
<div class="lh-audit-group__summary">
<div class="lh-audit-group__header">
<span class="lh-audit-group__title">Passed audits</span>
<span class="lh-audit-group__itemcount">(28)</span>
<!-- .lh-audit-group__description will be added here -->
<!-- .lh-metrics-toggle will be added here -->
</div>
<div class=""></div>
<svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</summary>
<div class="lh-audit lh-audit--numeric lh-audit--pass" id="render-blocking-resources">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Eliminate render-blocking resources</span></span>
<span class="lh-audit__display-text">Potential savings of 0&nbsp;ms</span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Resources are blocking the first paint of your page. Consider delivering critical JS/CSS inline and deferring all non-critical JS/styles. <a rel="noopener" target="_blank" href="https://web.dev/render-blocking-resources/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span><span class="lh-audit__adorn" title="Relevant to First Contentful Paint">FCP</span><span class="lh-audit__adorn" title="Relevant to Largest Contentful Paint">LCP</span></div>
<div class="lh-audit__stackpacks"></div>
<div class="lh-3p-filter">
<label class="lh-3p-filter-label" for="lh-3p-filter-label--3">
<input type="checkbox" class="lh-3p-filter-input" checked="" id="lh-3p-filter-label--3" disabled="">
<span class="lh-3p-ui-string">Show 3rd-party resources</span> (<span class="lh-3p-filter-count">0</span>)
</label>
</div>
<table class="lh-table lh-details"><thead><tr><th class="lh-table-column--url"><div class="lh-text">URL</div></th><th class="lh-table-column--bytes"><div class="lh-text">Transfer Size</div></th><th class="lh-table-column--timespanMs"><div class="lh-text">Potential Savings</div></th></tr></thead><tbody><tr class="lh-row--even"><td class="lh-table-column--url"><div class="lh-text__url" title="http://localhost:8008/congo/css/main.bundle.min.6783e342ffedbcc698d9afb9ffe11276f4f9de79bfc45cf670b35d5f9948d266086106596bc9e59820d7de0959e5889ce97e073fbafd380be9dc7b33b75295f2.css" data-url="http://localhost:8008/congo/css/main.bundle.min.6783e342ffedbcc698d9afb9ffe11276f4f9de79bfc45cf670b35d5f9948d266086106596bc9e59820d7de0959e5889ce97e073fbafd380be9dc7b33b75295f2.css"><a rel="noopener" target="_blank" href="http://localhost:8008/congo/css/main.bundle.min.6783e342ffedbcc698d9afb9ffe11276f4f9de79bfc45cf670b35d5f9948d266086106596bc9e59820d7de0959e5889ce97e073fbafd380be9dc7b33b75295f2.css" class="lh-link">…css/main.bundle.min.6783e34….css</a><div class="lh-text lh-text__url-host">(localhost)</div></div></td><td class="lh-table-column--bytes"><div class="lh-text" title="40,665&nbsp;bytes">39.7&nbsp;KiB</div></td><td class="lh-table-column--timespanMs"><div class="lh-text">480&nbsp;ms</div></td></tr></tbody></table></details>
</div><div class="lh-audit lh-audit--numeric lh-audit--pass" id="uses-responsive-images">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Properly size images</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Serve images that are appropriately-sized to save cellular data and improve load time. <a rel="noopener" target="_blank" href="https://web.dev/uses-responsive-images/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--numeric lh-audit--pass" id="offscreen-images">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Defer offscreen images</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Consider lazy-loading offscreen and hidden images after all critical resources have finished loading to lower time to interactive. <a rel="noopener" target="_blank" href="https://web.dev/offscreen-images/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--numeric lh-audit--pass" id="unminified-css">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Minify CSS</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Minifying CSS files can reduce network payload sizes. <a rel="noopener" target="_blank" href="https://web.dev/unminified-css/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span><span class="lh-audit__adorn" title="Relevant to First Contentful Paint">FCP</span><span class="lh-audit__adorn" title="Relevant to Largest Contentful Paint">LCP</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--numeric lh-audit--pass" id="unminified-javascript">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Minify JavaScript</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Minifying JavaScript files can reduce payload sizes and script parse time. <a rel="noopener" target="_blank" href="https://web.dev/unminified-javascript/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span><span class="lh-audit__adorn" title="Relevant to First Contentful Paint">FCP</span><span class="lh-audit__adorn" title="Relevant to Largest Contentful Paint">LCP</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--numeric lh-audit--pass" id="uses-optimized-images">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Efficiently encode images</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Optimized images load faster and consume less cellular data. <a rel="noopener" target="_blank" href="https://web.dev/uses-optimized-images/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--numeric lh-audit--pass" id="modern-image-formats">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Serve images in next-gen formats</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>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. <a rel="noopener" target="_blank" href="https://web.dev/uses-webp-images/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--numeric lh-audit--pass" id="uses-rel-preconnect">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Preconnect to required origins</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Consider adding `preconnect` or `dns-prefetch` resource hints to establish early connections to important third-party origins. <a rel="noopener" target="_blank" href="https://web.dev/uses-rel-preconnect/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span><span class="lh-audit__adorn" title="Relevant to First Contentful Paint">FCP</span><span class="lh-audit__adorn" title="Relevant to Largest Contentful Paint">LCP</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="server-response-time">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Initial server response time was short</span></span>
<span class="lh-audit__display-text">Root document took 0&nbsp;ms</span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Keep the server response time for the main document short because all other requests depend on it. <a rel="noopener" target="_blank" href="https://web.dev/time-to-first-byte/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span><span class="lh-audit__adorn" title="Relevant to First Contentful Paint">FCP</span><span class="lh-audit__adorn" title="Relevant to Largest Contentful Paint">LCP</span></div>
<div class="lh-audit__stackpacks"></div>
<div class="lh-3p-filter">
<label class="lh-3p-filter-label" for="lh-3p-filter-label--4">
<input type="checkbox" class="lh-3p-filter-input" checked="" id="lh-3p-filter-label--4" disabled="">
<span class="lh-3p-ui-string">Show 3rd-party resources</span> (<span class="lh-3p-filter-count">0</span>)
</label>
</div>
<table class="lh-table lh-details"><thead><tr><th class="lh-table-column--url"><div class="lh-text">URL</div></th><th class="lh-table-column--timespanMs"><div class="lh-text">Time Spent</div></th></tr></thead><tbody><tr class="lh-row--even"><td class="lh-table-column--url"><div class="lh-text__url" title="http://localhost:8008/congo/samples/emoji/" data-url="http://localhost:8008/congo/samples/emoji/"><a rel="noopener" target="_blank" href="http://localhost:8008/congo/samples/emoji/" class="lh-link">…samples/emoji</a><div class="lh-text lh-text__url-host">(localhost)</div></div></td><td class="lh-table-column--timespanMs"><div class="lh-text">0&nbsp;ms</div></td></tr></tbody></table></details>
</div><div class="lh-audit lh-audit--numeric lh-audit--pass" id="redirects">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Avoid multiple page redirects</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Redirects introduce additional delays before the page can be loaded. <a rel="noopener" target="_blank" href="https://web.dev/redirects/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span><span class="lh-audit__adorn" title="Relevant to First Contentful Paint">FCP</span><span class="lh-audit__adorn" title="Relevant to Largest Contentful Paint">LCP</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--numeric lh-audit--pass" id="uses-rel-preload">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Preload key requests</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Consider using `&lt;link rel=preload&gt;` to prioritize fetching resources that are currently requested later in page load. <a rel="noopener" target="_blank" href="https://web.dev/uses-rel-preload/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span><span class="lh-audit__adorn" title="Relevant to First Contentful Paint">FCP</span><span class="lh-audit__adorn" title="Relevant to Largest Contentful Paint">LCP</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="uses-http2">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Use HTTP/2</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>HTTP/2 offers many benefits over HTTP/1.1, including binary headers and multiplexing. <a rel="noopener" target="_blank" href="https://web.dev/uses-http2/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--numeric lh-audit--pass" id="efficient-animated-content">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Use video formats for animated content</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>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. <a rel="noopener" target="_blank" href="https://web.dev/efficient-animated-content/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a></span><span class="lh-audit__adorn" title="Relevant to Largest Contentful Paint">LCP</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--numeric lh-audit--pass" id="duplicated-javascript">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Remove duplicate modules in JavaScript bundles</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Remove large, duplicate JavaScript modules from bundles to reduce unnecessary bytes consumed by network activity. </span><span class="lh-audit__adorn" title="Relevant to Total Blocking Time">TBT</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--numeric lh-audit--pass" id="legacy-javascript">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Avoid serving legacy JavaScript to modern browsers</span></span>
<span class="lh-audit__display-text">Potential savings of 0&nbsp;KiB</span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Polyfills and transforms enable legacy browsers to use new JavaScript features. However, many aren't necessary for modern browsers. For your bundled JavaScript, adopt a modern script deployment strategy using module/nomodule feature detection to reduce the amount of code shipped to modern browsers, while retaining support for legacy browsers. <a rel="noopener" target="_blank" href="https://philipwalton.com/articles/deploying-es2015-code-in-production-today/">Learn More</a></span><span class="lh-audit__adorn" title="Relevant to Total Blocking Time">TBT</span></div>
<div class="lh-audit__stackpacks"></div>
<div class="lh-3p-filter">
<label class="lh-3p-filter-label" for="lh-3p-filter-label--5">
<input type="checkbox" class="lh-3p-filter-input" checked="" id="lh-3p-filter-label--5" disabled="">
<span class="lh-3p-ui-string">Show 3rd-party resources</span> (<span class="lh-3p-filter-count">0</span>)
</label>
</div>
<table class="lh-table lh-details"><thead><tr><th class="lh-table-column--url"><div class="lh-text">URL</div></th><th class="lh-table-column--code"><div class="lh-text"></div></th><th class="lh-table-column--bytes"><div class="lh-text">Potential Savings</div></th></tr></thead><tbody><tr class="lh-row--even"><td class="lh-table-column--url"><div class="lh-text__url" title="http://localhost:8008/congo/js/main.bundle.min.81d5d44787f3f7c0220442722b6d0c72da50dab39103321d578502e81b226ed5a87e765e5e2a7c0533fdddeb4697bd499a39be89310453fba5c129f5ba69bdea.js" data-url="http://localhost:8008/congo/js/main.bundle.min.81d5d44787f3f7c0220442722b6d0c72da50dab39103321d578502e81b226ed5a87e765e5e2a7c0533fdddeb4697bd499a39be89310453fba5c129f5ba69bdea.js"><a rel="noopener" target="_blank" href="http://localhost:8008/congo/js/main.bundle.min.81d5d44787f3f7c0220442722b6d0c72da50dab39103321d578502e81b226ed5a87e765e5e2a7c0533fdddeb4697bd499a39be89310453fba5c129f5ba69bdea.js" class="lh-link">…js/main.bundle.min.81d5d44….js</a><div class="lh-text lh-text__url-host">(localhost)</div></div></td><td class="lh-table-column--empty"></td><td class="lh-table-column--bytes"><div class="lh-text" title="168&nbsp;bytes">0.2&nbsp;KiB</div></td></tr><tr class="lh-sub-item-row lh-row--even"><td class="lh-table-column--source-location"><div class="lh-text__url lh-source-location" title="http://localhost:8008/congo/js/main.bundle.min.81d5d44787f3f7c0220442722b6d0c72da50dab39103321d578502e81b226ed5a87e765e5e2a7c0533fdddeb4697bd499a39be89310453fba5c129f5ba69bdea.js" data-url="http://localhost:8008/congo/js/main.bundle.min.81d5d44787f3f7c0220442722b6d0c72da50dab39103321d578502e81b226ed5a87e765e5e2a7c0533fdddeb4697bd499a39be89310453fba5c129f5ba69bdea.js" data-source-url="http://localhost:8008/congo/js/main.bundle.min.81d5d44787f3f7c0220442722b6d0c72da50dab39103321d578502e81b226ed5a87e765e5e2a7c0533fdddeb4697bd499a39be89310453fba5c129f5ba69bdea.js" data-source-line="0" data-source-column="2227"><a rel="noopener" target="_blank" href="http://localhost:8008/congo/js/main.bundle.min.81d5d44787f3f7c0220442722b6d0c72da50dab39103321d578502e81b226ed5a87e765e5e2a7c0533fdddeb4697bd499a39be89310453fba5c129f5ba69bdea.js" class="lh-link">…js/main.bundle.min.81d5d44….js:1:2227</a><div class="lh-text lh-text__url-host">(localhost)</div></div></td><td class="lh-table-column--code"><pre class="lh-code">@babel/plugin-transform-classes</pre></td><td class="lh-table-column--empty"></td></tr></tbody></table></details>
</div><div class="lh-audit lh-audit--numeric lh-audit--pass" id="preload-lcp-image">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Preload Largest Contentful Paint image</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Preload the image used by the LCP element in order to improve your LCP time. <a rel="noopener" target="_blank" href="https://web.dev/optimize-lcp/?utm_source=lighthouse&amp;utm_medium=cli#preload-important-resources">Learn more</a>.</span><span class="lh-audit__adorn" title="Relevant to Largest Contentful Paint">LCP</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--numeric lh-audit--pass" id="total-byte-weight">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Avoids enormous network payloads</span></span>
<span class="lh-audit__display-text">Total size was 92&nbsp;KiB</span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Large network payloads cost users real money and are highly correlated with long load times. <a rel="noopener" target="_blank" href="https://web.dev/total-byte-weight/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span><span class="lh-audit__adorn" title="Relevant to Largest Contentful Paint">LCP</span></div>
<div class="lh-audit__stackpacks"></div>
<div class="lh-3p-filter">
<label class="lh-3p-filter-label" for="lh-3p-filter-label--6">
<input type="checkbox" class="lh-3p-filter-input" checked="" id="lh-3p-filter-label--6" disabled="">
<span class="lh-3p-ui-string">Show 3rd-party resources</span> (<span class="lh-3p-filter-count">0</span>)
</label>
</div>
<table class="lh-table lh-details"><thead><tr><th class="lh-table-column--url"><div class="lh-text">URL</div></th><th class="lh-table-column--bytes"><div class="lh-text">Transfer Size</div></th></tr></thead><tbody><tr class="lh-row--even"><td class="lh-table-column--url"><div class="lh-text__url" title="http://localhost:8008/congo/css/main.bundle.min.6783e342ffedbcc698d9afb9ffe11276f4f9de79bfc45cf670b35d5f9948d266086106596bc9e59820d7de0959e5889ce97e073fbafd380be9dc7b33b75295f2.css" data-url="http://localhost:8008/congo/css/main.bundle.min.6783e342ffedbcc698d9afb9ffe11276f4f9de79bfc45cf670b35d5f9948d266086106596bc9e59820d7de0959e5889ce97e073fbafd380be9dc7b33b75295f2.css"><a rel="noopener" target="_blank" href="http://localhost:8008/congo/css/main.bundle.min.6783e342ffedbcc698d9afb9ffe11276f4f9de79bfc45cf670b35d5f9948d266086106596bc9e59820d7de0959e5889ce97e073fbafd380be9dc7b33b75295f2.css" class="lh-link">…css/main.bundle.min.6783e34….css</a><div class="lh-text lh-text__url-host">(localhost)</div></div></td><td class="lh-table-column--bytes"><div class="lh-text" title="40,665&nbsp;bytes">39.7&nbsp;KiB</div></td></tr><tr class="lh-row--odd"><td class="lh-table-column--url"><div class="lh-text__url" title="http://localhost:8008/congo/js/main.bundle.min.81d5d44787f3f7c0220442722b6d0c72da50dab39103321d578502e81b226ed5a87e765e5e2a7c0533fdddeb4697bd499a39be89310453fba5c129f5ba69bdea.js" data-url="http://localhost:8008/congo/js/main.bundle.min.81d5d44787f3f7c0220442722b6d0c72da50dab39103321d578502e81b226ed5a87e765e5e2a7c0533fdddeb4697bd499a39be89310453fba5c129f5ba69bdea.js"><a rel="noopener" target="_blank" href="http://localhost:8008/congo/js/main.bundle.min.81d5d44787f3f7c0220442722b6d0c72da50dab39103321d578502e81b226ed5a87e765e5e2a7c0533fdddeb4697bd499a39be89310453fba5c129f5ba69bdea.js" class="lh-link">…js/main.bundle.min.81d5d44….js</a><div class="lh-text lh-text__url-host">(localhost)</div></div></td><td class="lh-table-column--bytes"><div class="lh-text" title="27,861&nbsp;bytes">27.2&nbsp;KiB</div></td></tr><tr class="lh-row--even"><td class="lh-table-column--url"><div class="lh-text__url" title="http://localhost:8008/congo/samples/emoji/" data-url="http://localhost:8008/congo/samples/emoji/"><a rel="noopener" target="_blank" href="http://localhost:8008/congo/samples/emoji/" class="lh-link">…samples/emoji</a><div class="lh-text lh-text__url-host">(localhost)</div></div></td><td class="lh-table-column--bytes"><div class="lh-text" title="17,411&nbsp;bytes">17.0&nbsp;KiB</div></td></tr><tr class="lh-row--odd"><td class="lh-table-column--url"><div class="lh-text__url" title="http://localhost:8008/congo/author_hu052d24d60828c1cca42a6aefe45b8b44_54195_192x192_fill_q75_box_smart1.jpg" data-url="http://localhost:8008/congo/author_hu052d24d60828c1cca42a6aefe45b8b44_54195_192x192_fill_q75_box_smart1.jpg"><a rel="noopener" target="_blank" href="http://localhost:8008/congo/author_hu052d24d60828c1cca42a6aefe45b8b44_54195_192x192_fill_q75_box_smart1.jpg" class="lh-link">/congo/author_hu052d24d…_54195_192x192_fill_q75_box_smart1.jpg</a><div class="lh-text lh-text__url-host">(localhost)</div></div></td><td class="lh-table-column--bytes"><div class="lh-text" title="7,915&nbsp;bytes">7.7&nbsp;KiB</div></td></tr></tbody></table></details>
</div><div class="lh-audit lh-audit--numeric lh-audit--pass" id="dom-size">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Avoids an excessive DOM size</span></span>
<span class="lh-audit__display-text">120 elements</span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>A large DOM will increase memory usage, cause longer <a rel="noopener" target="_blank" href="https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations?utm_source=lighthouse&amp;utm_medium=cli">style calculations</a>, and produce costly <a rel="noopener" target="_blank" href="https://developers.google.com/speed/articles/reflow?utm_source=lighthouse&amp;utm_medium=cli">layout reflows</a>. <a rel="noopener" target="_blank" href="https://web.dev/dom-size/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span><span class="lh-audit__adorn" title="Relevant to Total Blocking Time">TBT</span></div>
<div class="lh-audit__stackpacks"></div>
<table class="lh-table lh-details"><thead><tr><th class="lh-table-column--text"><div class="lh-text">Statistic</div></th><th class="lh-table-column--node"><div class="lh-text">Element</div></th><th class="lh-table-column--numeric"><div class="lh-text">Value</div></th></tr></thead><tbody><tr class="lh-row--even"><td class="lh-table-column--text"><div class="lh-text">Total DOM Elements</div></td><td class="lh-table-column--empty"></td><td class="lh-table-column--numeric"><div class="lh-numeric">120</div></td></tr><tr class="lh-row--odd"><td class="lh-table-column--text"><div class="lh-text">Maximum DOM Depth</div></td><td class="lh-table-column--node"><span class="lh-node" title="a.px-1 > span.relative > svg.svg-inline--fa > path" data-path="1,HTML,1,BODY,2,MAIN,0,ARTICLE,2,FOOTER,0,DIV,1,DIV,3,DIV,0,DIV,0,A,0,SPAN,0,svg,0,path" data-selector="a.px-1 > span.relative > svg.svg-inline--fa > path" data-snippet="<path fill=&quot;currentcolor&quot; d=&quot;M459.37 151.716c.325 4.548.325 9.097.325 13.645.0 138.72-105.583 298.558-2…&quot;>"><div>path</div><div class="lh-node__snippet">&lt;path fill="currentcolor" d="M459.37 151.716c.325 4.548.325 9.097.325 13.645.0 138.72-105.583 298.558-2…"&gt;</div></span></td><td class="lh-table-column--numeric"><div class="lh-numeric">12</div></td></tr><tr class="lh-row--even"><td class="lh-table-column--text"><div class="lh-text">Maximum Child Elements</div></td><td class="lh-table-column--node"><span class="lh-node" title="body.flex" data-path="1,HTML,1,BODY" data-selector="body.flex" data-snippet="<body class=&quot;flex flex-col h-screen px-6 m-auto text-lg leading-7 bg-neutral text-neutr…&quot;>"><div>body</div><div class="lh-node__snippet">&lt;body class="flex flex-col h-screen px-6 m-auto text-lg leading-7 bg-neutral text-neutr…"&gt;</div></span></td><td class="lh-table-column--numeric"><div class="lh-numeric">5</div></td></tr></tbody></table></details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="user-timings">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>User Timing marks and measures</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Consider instrumenting your app with the User Timing API to measure your app's real-world performance during key user experiences. <a rel="noopener" target="_blank" href="https://web.dev/user-timings/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--numeric lh-audit--pass" id="bootup-time">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>JavaScript execution time</span></span>
<span class="lh-audit__display-text">0.0&nbsp;s</span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Consider reducing the time spent parsing, compiling, and executing JS. You may find delivering smaller JS payloads helps with this. <a rel="noopener" target="_blank" href="https://web.dev/bootup-time/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span><span class="lh-audit__adorn" title="Relevant to Total Blocking Time">TBT</span></div>
<div class="lh-audit__stackpacks"></div>
<div class="lh-3p-filter">
<label class="lh-3p-filter-label" for="lh-3p-filter-label--7">
<input type="checkbox" class="lh-3p-filter-input" checked="" id="lh-3p-filter-label--7" disabled="">
<span class="lh-3p-ui-string">Show 3rd-party resources</span> (<span class="lh-3p-filter-count">0</span>)
</label>
</div>
<table class="lh-table lh-details"><thead><tr><th class="lh-table-column--url"><div class="lh-text">URL</div></th><th class="lh-table-column--ms"><div class="lh-text">Total CPU Time</div></th><th class="lh-table-column--ms"><div class="lh-text">Script Evaluation</div></th><th class="lh-table-column--ms"><div class="lh-text">Script Parse</div></th></tr></thead><tbody><tr class="lh-row--even"><td class="lh-table-column--url"><div class="lh-text__url" title="http://localhost:8008/congo/samples/emoji/" data-url="http://localhost:8008/congo/samples/emoji/"><a rel="noopener" target="_blank" href="http://localhost:8008/congo/samples/emoji/" class="lh-link">…samples/emoji</a><div class="lh-text lh-text__url-host">(localhost)</div></div></td><td class="lh-table-column--ms"><div class="lh-text">1,411&nbsp;ms</div></td><td class="lh-table-column--ms"><div class="lh-text">4&nbsp;ms</div></td><td class="lh-table-column--ms"><div class="lh-text">1&nbsp;ms</div></td></tr></tbody></table></details>
</div><div class="lh-audit lh-audit--numeric lh-audit--pass" id="mainthread-work-breakdown">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Minimizes main-thread work</span></span>
<span class="lh-audit__display-text">1.5&nbsp;s</span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Consider reducing the time spent parsing, compiling and executing JS. You may find delivering smaller JS payloads helps with this. <a rel="noopener" target="_blank" href="https://web.dev/mainthread-work-breakdown/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a></span><span class="lh-audit__adorn" title="Relevant to Total Blocking Time">TBT</span></div>
<div class="lh-audit__stackpacks"></div>
<table class="lh-table lh-details"><thead><tr><th class="lh-table-column--text"><div class="lh-text">Category</div></th><th class="lh-table-column--ms"><div class="lh-text">Time Spent</div></th></tr></thead><tbody><tr class="lh-row--even"><td class="lh-table-column--text"><div class="lh-text">Style &amp; Layout</div></td><td class="lh-table-column--ms"><div class="lh-text">1,117&nbsp;ms</div></td></tr><tr class="lh-row--odd"><td class="lh-table-column--text"><div class="lh-text">Rendering</div></td><td class="lh-table-column--ms"><div class="lh-text">252&nbsp;ms</div></td></tr><tr class="lh-row--even"><td class="lh-table-column--text"><div class="lh-text">Other</div></td><td class="lh-table-column--ms"><div class="lh-text">53&nbsp;ms</div></td></tr><tr class="lh-row--odd"><td class="lh-table-column--text"><div class="lh-text">Script Evaluation</div></td><td class="lh-table-column--ms"><div class="lh-text">20&nbsp;ms</div></td></tr><tr class="lh-row--even"><td class="lh-table-column--text"><div class="lh-text">Parse HTML &amp; CSS</div></td><td class="lh-table-column--ms"><div class="lh-text">8&nbsp;ms</div></td></tr><tr class="lh-row--odd"><td class="lh-table-column--text"><div class="lh-text">Script Parsing &amp; Compilation</div></td><td class="lh-table-column--ms"><div class="lh-text">3&nbsp;ms</div></td></tr></tbody></table></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="font-display">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>All text remains visible during webfont loads</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Leverage the font-display CSS feature to ensure text is user-visible while webfonts are loading. <a rel="noopener" target="_blank" href="https://web.dev/font-display/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span><span class="lh-audit__adorn" title="Relevant to First Contentful Paint">FCP</span><span class="lh-audit__adorn" title="Relevant to Largest Contentful Paint">LCP</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="third-party-summary">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Minimize third-party usage</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Third-party code can significantly impact load performance. Limit the number of redundant third-party providers and try to load third-party code after your page has primarily finished loading. <a rel="noopener" target="_blank" href="https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/loading-third-party-javascript/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span><span class="lh-audit__adorn" title="Relevant to Total Blocking Time">TBT</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="third-party-facades">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Lazy load third-party resources with facades</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Some third-party embeds can be lazy loaded. Consider replacing them with a facade until they are required. <a rel="noopener" target="_blank" href="https://web.dev/third-party-facades/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span><span class="lh-audit__adorn" title="Relevant to Total Blocking Time">TBT</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="layout-shift-elements">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Avoid large layout shifts</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>These DOM elements contribute most to the CLS of the page.</span><span class="lh-audit__adorn" title="Relevant to Cumulative Layout Shift">CLS</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="uses-passive-event-listeners">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Uses passive listeners to improve scrolling performance</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Consider marking your touch and wheel event listeners as `passive` to improve your page's scroll performance. <a rel="noopener" target="_blank" href="https://web.dev/uses-passive-event-listeners/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="no-document-write">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Avoids <code>document.write()</code></span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>For users on slow connections, external scripts dynamically injected via `document.write()` can delay page load by tens of seconds. <a rel="noopener" target="_blank" href="https://web.dev/no-document-write/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="unsized-images">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Image elements have explicit <code>width</code> and <code>height</code></span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Set an explicit width and height on image elements to reduce layout shifts and improve CLS. <a rel="noopener" target="_blank" href="https://web.dev/optimize-cls/?utm_source=lighthouse&amp;utm_medium=cli#images-without-dimensions">Learn more</a></span><span class="lh-audit__adorn" title="Relevant to Cumulative Layout Shift">CLS</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div></details></div></div><div class="lh-category-wrapper"><div class="lh-category"><span class="lh-permalink" id="accessibility"></span>
<div class="lh-category-header">
<div class="lh-score__gauge" role="heading" aria-level="2">
<a href="#accessibility" class="lh-gauge__wrapper lh-gauge__wrapper--pass">
<!-- Wrapper exists for the ::before plugin icon. Cannot create pseudo-elements on svgs. -->
<div class="lh-gauge__svg-wrapper">
<svg viewBox="0 0 120 120" class="lh-gauge">
<circle class="lh-gauge-base" r="56" cx="60" cy="60" stroke-width="8"></circle>
<circle class="lh-gauge-arc" r="56" cx="60" cy="60" stroke-width="8" style="transform: rotate(-87.95372216024705deg); stroke-dasharray: 351.85837720205683px, 351.85837720205683px;"></circle>
</svg>
</div>
<div class="lh-gauge__percentage">100</div>
<!-- TODO: should likely be an h2 -->
<div class="lh-gauge__label">Accessibility</div>
</a>
</div>
<div class="lh-category-header__description"><span>These checks highlight opportunities to <a rel="noopener" target="_blank" href="https://developers.google.com/web/fundamentals/accessibility?utm_source=lighthouse&amp;utm_medium=cli">improve the accessibility of your web app</a>. Only a subset of accessibility issues can be automatically detected so manual testing is also encouraged.</span></div>
</div>
<details class="lh-clump lh-audit-group lh-clump--manual">
<summary>
<div class="lh-audit-group__summary">
<div class="lh-audit-group__header">
<span class="lh-audit-group__title">Additional items to manually check</span>
<span class="lh-audit-group__itemcount">(10)</span>
<!-- .lh-audit-group__description will be added here -->
<!-- .lh-metrics-toggle will be added here -->
<span class="lh-audit-group__description">These items address areas which an automated testing tool cannot cover. Learn more in our guide on <a rel="noopener" target="_blank" href="https://developers.google.com/web/fundamentals/accessibility/how-to-review?utm_source=lighthouse&amp;utm_medium=cli">conducting an accessibility review</a>.</span></div>
<div class=""></div>
<svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</summary>
<div class="lh-audit lh-audit--manual lh-audit--pass" id="logical-tab-order">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>The page has a logical tab order</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Tabbing through the page follows the visual layout. Users cannot focus elements that are offscreen. <a rel="noopener" target="_blank" href="https://web.dev/logical-tab-order/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--manual lh-audit--pass" id="focusable-controls">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Interactive controls are keyboard focusable</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Custom interactive controls are keyboard focusable and display a focus indicator. <a rel="noopener" target="_blank" href="https://web.dev/focusable-controls/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--manual lh-audit--pass" id="interactive-element-affordance">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Interactive elements indicate their purpose and state</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Interactive elements, such as links and buttons, should indicate their state and be distinguishable from non-interactive elements. <a rel="noopener" target="_blank" href="https://web.dev/interactive-element-affordance/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--manual lh-audit--pass" id="managed-focus">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>The user's focus is directed to new content added to the page</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>If new content, such as a dialog, is added to the page, the user's focus is directed to it. <a rel="noopener" target="_blank" href="https://web.dev/managed-focus/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--manual lh-audit--pass" id="focus-traps">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>User focus is not accidentally trapped in a region</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>A user can tab into and out of any control or region without accidentally trapping their focus. <a rel="noopener" target="_blank" href="https://web.dev/focus-traps/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--manual lh-audit--pass" id="custom-controls-labels">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Custom controls have associated labels</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Custom interactive controls have associated labels, provided by aria-label or aria-labelledby. <a rel="noopener" target="_blank" href="https://web.dev/custom-controls-labels/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--manual lh-audit--pass" id="custom-controls-roles">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Custom controls have ARIA roles</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Custom interactive controls have appropriate ARIA roles. <a rel="noopener" target="_blank" href="https://web.dev/custom-control-roles/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--manual lh-audit--pass" id="visual-order-follows-dom">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Visual order on the page follows DOM order</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>DOM order matches the visual order, improving navigation for assistive technology. <a rel="noopener" target="_blank" href="https://web.dev/visual-order-follows-dom/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--manual lh-audit--pass" id="offscreen-content-hidden">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Offscreen content is hidden from assistive technology</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Offscreen content is hidden with display: none or aria-hidden=true. <a rel="noopener" target="_blank" href="https://web.dev/offscreen-content-hidden/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--manual lh-audit--pass" id="use-landmarks">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>HTML5 landmark elements are used to improve navigation</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Landmark elements (&lt;main&gt;, &lt;nav&gt;, etc.) are used to improve the keyboard navigation of the page for assistive technology. <a rel="noopener" target="_blank" href="https://web.dev/use-landmarks/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div></details><details class="lh-clump lh-audit-group lh-clump--passed">
<summary>
<div class="lh-audit-group__summary">
<div class="lh-audit-group__header">
<span class="lh-audit-group__title">Passed audits</span>
<span class="lh-audit-group__itemcount">(18)</span>
<!-- .lh-audit-group__description will be added here -->
<!-- .lh-metrics-toggle will be added here -->
</div>
<div class=""></div>
<svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</summary>
<div class="lh-audit lh-audit--binary lh-audit--pass" id="aria-allowed-attr">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>[aria-*]</code> attributes match their roles</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Each ARIA `role` supports a specific subset of `aria-*` attributes. Mismatching these invalidates the `aria-*` attributes. <a rel="noopener" target="_blank" href="https://web.dev/aria-allowed-attr/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="aria-hidden-body">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>[aria-hidden="true"]</code> is not present on the document <code>&lt;body&gt;</code></span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Assistive technologies, like screen readers, work inconsistently when `aria-hidden="true"` is set on the document `&lt;body&gt;`. <a rel="noopener" target="_blank" href="https://web.dev/aria-hidden-body/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="aria-hidden-focus">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>[aria-hidden="true"]</code> elements do not contain focusable descendents</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Focusable descendents within an `[aria-hidden="true"]` element prevent those interactive elements from being available to users of assistive technologies like screen readers. <a rel="noopener" target="_blank" href="https://web.dev/aria-hidden-focus/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="aria-valid-attr-value">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>[aria-*]</code> attributes have valid values</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Assistive technologies, like screen readers, can't interpret ARIA attributes with invalid values. <a rel="noopener" target="_blank" href="https://web.dev/aria-valid-attr-value/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="aria-valid-attr">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>[aria-*]</code> attributes are valid and not misspelled</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Assistive technologies, like screen readers, can't interpret ARIA attributes with invalid names. <a rel="noopener" target="_blank" href="https://web.dev/aria-valid-attr/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="button-name">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Buttons have an accessible name</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>When a button doesn't have an accessible name, screen readers announce it as "button", making it unusable for users who rely on screen readers. <a rel="noopener" target="_blank" href="https://web.dev/button-name/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="bypass">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>The page contains a heading, skip link, or landmark region</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Adding ways to bypass repetitive content lets keyboard users navigate the page more efficiently. <a rel="noopener" target="_blank" href="https://web.dev/bypass/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="color-contrast">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Background and foreground colors have a sufficient contrast ratio</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Low-contrast text is difficult or impossible for many users to read. <a rel="noopener" target="_blank" href="https://web.dev/color-contrast/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="document-title">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Document has a <code>&lt;title&gt;</code> element</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>The title gives screen reader users an overview of the page, and search engine users rely on it heavily to determine if a page is relevant to their search. <a rel="noopener" target="_blank" href="https://web.dev/document-title/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="duplicate-id-active">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>[id]</code> attributes on active, focusable elements are unique</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>All focusable elements must have a unique `id` to ensure that they're visible to assistive technologies. <a rel="noopener" target="_blank" href="https://web.dev/duplicate-id-active/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="heading-order">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Heading elements appear in a sequentially-descending order</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Properly ordered headings that do not skip levels convey the semantic structure of the page, making it easier to navigate and understand when using assistive technologies. <a rel="noopener" target="_blank" href="https://web.dev/heading-order/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="html-has-lang">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>&lt;html&gt;</code> element has a <code>[lang]</code> attribute</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>If a page doesn't specify a lang attribute, a screen reader assumes that the page is in the default language that the user chose when setting up the screen reader. If the page isn't actually in the default language, then the screen reader might not announce the page's text correctly. <a rel="noopener" target="_blank" href="https://web.dev/html-has-lang/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="html-lang-valid">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>&lt;html&gt;</code> element has a valid value for its <code>[lang]</code> attribute</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Specifying a valid <a rel="noopener" target="_blank" href="https://www.w3.org/International/questions/qa-choosing-language-tags#question">BCP 47 language</a> helps screen readers announce text properly. <a rel="noopener" target="_blank" href="https://web.dev/html-lang-valid/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="image-alt">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Image elements have <code>[alt]</code> attributes</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Informative elements should aim for short, descriptive alternate text. Decorative elements can be ignored with an empty alt attribute. <a rel="noopener" target="_blank" href="https://web.dev/image-alt/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="link-name">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Links have a discernible name</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Link text (and alternate text for images, when used as links) that is discernible, unique, and focusable improves the navigation experience for screen reader users. <a rel="noopener" target="_blank" href="https://web.dev/link-name/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="list">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Lists contain only <code>&lt;li&gt;</code> elements and script supporting elements (<code>&lt;script&gt;</code> and <code>&lt;template&gt;</code>).</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Screen readers have a specific way of announcing lists. Ensuring proper list structure aids screen reader output. <a rel="noopener" target="_blank" href="https://web.dev/list/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="listitem">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>List items (<code>&lt;li&gt;</code>) are contained within <code>&lt;ul&gt;</code> or <code>&lt;ol&gt;</code> parent elements</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Screen readers require list items (`&lt;li&gt;`) to be contained within a parent `&lt;ul&gt;` or `&lt;ol&gt;` to be announced properly. <a rel="noopener" target="_blank" href="https://web.dev/listitem/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="meta-viewport">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>[user-scalable="no"]</code> is not used in the <code>&lt;meta name="viewport"&gt;</code> element and the <code>[maximum-scale]</code> attribute is not less than 5.</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Disabling zooming is problematic for users with low vision who rely on screen magnification to properly see the contents of a web page. <a rel="noopener" target="_blank" href="https://web.dev/meta-viewport/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div></details><details class="lh-clump lh-audit-group lh-clump--notapplicable">
<summary>
<div class="lh-audit-group__summary">
<div class="lh-audit-group__header">
<span class="lh-audit-group__title">Not applicable</span>
<span class="lh-audit-group__itemcount">(26)</span>
<!-- .lh-audit-group__description will be added here -->
<!-- .lh-metrics-toggle will be added here -->
</div>
<div class=""></div>
<svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</summary>
<div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="accesskeys">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>[accesskey]</code> values are unique</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Access keys let users quickly focus a part of the page. For proper navigation, each access key must be unique. <a rel="noopener" target="_blank" href="https://web.dev/accesskeys/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="aria-command-name">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>button</code>, <code>link</code>, and <code>menuitem</code> elements have accessible names</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. <a rel="noopener" target="_blank" href="https://web.dev/aria-name/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="aria-input-field-name">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>ARIA input fields have accessible names</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>When an input field doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. <a rel="noopener" target="_blank" href="https://web.dev/aria-name/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="aria-meter-name">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>ARIA <code>meter</code> elements have accessible names</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. <a rel="noopener" target="_blank" href="https://web.dev/aria-name/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="aria-progressbar-name">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>ARIA <code>progressbar</code> elements have accessible names</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. <a rel="noopener" target="_blank" href="https://web.dev/aria-name/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="aria-required-attr">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>[role]</code>s have all required <code>[aria-*]</code> attributes</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Some ARIA roles have required attributes that describe the state of the element to screen readers. <a rel="noopener" target="_blank" href="https://web.dev/aria-required-attr/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="aria-required-children">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Elements with an ARIA <code>[role]</code> that require children to contain a specific <code>[role]</code> have all required children.</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Some ARIA parent roles must contain specific child roles to perform their intended accessibility functions. <a rel="noopener" target="_blank" href="https://web.dev/aria-required-children/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="aria-required-parent">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>[role]</code>s are contained by their required parent element</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Some ARIA child roles must be contained by specific parent roles to properly perform their intended accessibility functions. <a rel="noopener" target="_blank" href="https://web.dev/aria-required-parent/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="aria-roles">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>[role]</code> values are valid</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>ARIA roles must have valid values in order to perform their intended accessibility functions. <a rel="noopener" target="_blank" href="https://web.dev/aria-roles/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="aria-toggle-field-name">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>ARIA toggle fields have accessible names</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>When a toggle field doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. <a rel="noopener" target="_blank" href="https://web.dev/aria-name/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="aria-tooltip-name">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>ARIA <code>tooltip</code> elements have accessible names</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. <a rel="noopener" target="_blank" href="https://web.dev/aria-name/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="aria-treeitem-name">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>ARIA <code>treeitem</code> elements have accessible names</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. <a rel="noopener" target="_blank" href="https://web.dev/aria-name/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="definition-list">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>&lt;dl&gt;</code>'s contain only properly-ordered <code>&lt;dt&gt;</code> and <code>&lt;dd&gt;</code> groups, <code>&lt;script&gt;</code>, <code>&lt;template&gt;</code> or <code>&lt;div&gt;</code> elements.</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>When definition lists are not properly marked up, screen readers may produce confusing or inaccurate output. <a rel="noopener" target="_blank" href="https://web.dev/definition-list/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="dlitem">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Definition list items are wrapped in <code>&lt;dl&gt;</code> elements</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Definition list items (`&lt;dt&gt;` and `&lt;dd&gt;`) must be wrapped in a parent `&lt;dl&gt;` element to ensure that screen readers can properly announce them. <a rel="noopener" target="_blank" href="https://web.dev/dlitem/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="duplicate-id-aria">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>ARIA IDs are unique</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>The value of an ARIA ID must be unique to prevent other instances from being overlooked by assistive technologies. <a rel="noopener" target="_blank" href="https://web.dev/duplicate-id-aria/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="form-field-multiple-labels">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>No form fields have multiple labels</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Form fields with multiple labels can be confusingly announced by assistive technologies like screen readers which use either the first, the last, or all of the labels. <a rel="noopener" target="_blank" href="https://web.dev/form-field-multiple-labels/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="frame-title">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>&lt;frame&gt;</code> or <code>&lt;iframe&gt;</code> elements have a title</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Screen reader users rely on frame titles to describe the contents of frames. <a rel="noopener" target="_blank" href="https://web.dev/frame-title/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="input-image-alt">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>&lt;input type="image"&gt;</code> elements have <code>[alt]</code> text</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>When an image is being used as an `&lt;input&gt;` button, providing alternative text can help screen reader users understand the purpose of the button. <a rel="noopener" target="_blank" href="https://web.dev/input-image-alt/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="label">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Form elements have associated labels</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Labels ensure that form controls are announced properly by assistive technologies, like screen readers. <a rel="noopener" target="_blank" href="https://web.dev/label/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="meta-refresh">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>The document does not use <code>&lt;meta http-equiv="refresh"&gt;</code></span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Users do not expect a page to refresh automatically, and doing so will move focus back to the top of the page. This may create a frustrating or confusing experience. <a rel="noopener" target="_blank" href="https://web.dev/meta-refresh/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="object-alt">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>&lt;object&gt;</code> elements have <code>[alt]</code> text</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Screen readers cannot translate non-text content. Adding alt text to `&lt;object&gt;` elements helps screen readers convey meaning to users. <a rel="noopener" target="_blank" href="https://web.dev/object-alt/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="tabindex">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>No element has a <code>[tabindex]</code> value greater than 0</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>A value greater than 0 implies an explicit navigation ordering. Although technically valid, this often creates frustrating experiences for users who rely on assistive technologies. <a rel="noopener" target="_blank" href="https://web.dev/tabindex/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="td-headers-attr">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Cells in a <code>&lt;table&gt;</code> element that use the <code>[headers]</code> attribute refer to table cells within the same table.</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Screen readers have features to make navigating tables easier. Ensuring `&lt;td&gt;` cells using the `[headers]` attribute only refer to other cells in the same table may improve the experience for screen reader users. <a rel="noopener" target="_blank" href="https://web.dev/td-headers-attr/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="th-has-data-cells">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>&lt;th&gt;</code> elements and elements with <code>[role="columnheader"/"rowheader"]</code> have data cells they describe.</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Screen readers have features to make navigating tables easier. Ensuring table headers always refer to some set of cells may improve the experience for screen reader users. <a rel="noopener" target="_blank" href="https://web.dev/th-has-data-cells/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="valid-lang">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>[lang]</code> attributes have a valid value</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Specifying a valid <a rel="noopener" target="_blank" href="https://www.w3.org/International/questions/qa-choosing-language-tags#question">BCP 47 language</a> on elements helps ensure that text is pronounced correctly by a screen reader. <a rel="noopener" target="_blank" href="https://web.dev/valid-lang/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="video-caption">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span><code>&lt;video&gt;</code> elements contain a <code>&lt;track&gt;</code> element with <code>[kind="captions"]</code></span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>When a video provides a caption it is easier for deaf and hearing impaired users to access its information. <a rel="noopener" target="_blank" href="https://web.dev/video-caption/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div></details></div></div><div class="lh-category-wrapper"><div class="lh-category"><span class="lh-permalink" id="best-practices"></span>
<div class="lh-category-header">
<div class="lh-score__gauge" role="heading" aria-level="2">
<a href="#best-practices" class="lh-gauge__wrapper lh-gauge__wrapper--pass">
<!-- Wrapper exists for the ::before plugin icon. Cannot create pseudo-elements on svgs. -->
<div class="lh-gauge__svg-wrapper">
<svg viewBox="0 0 120 120" class="lh-gauge">
<circle class="lh-gauge-base" r="56" cx="60" cy="60" stroke-width="8"></circle>
<circle class="lh-gauge-arc" r="56" cx="60" cy="60" stroke-width="8" style="transform: rotate(-87.95372216024705deg); stroke-dasharray: 351.85837720205683px, 351.85837720205683px;"></circle>
</svg>
</div>
<div class="lh-gauge__percentage">100</div>
<!-- TODO: should likely be an h2 -->
<div class="lh-gauge__label">Best Practices</div>
</a>
</div>
<div class="lh-category-header__description"></div>
</div>
<div class="lh-clump--failed"><div class="lh-audit-group lh-audit-group--best-practices-trust-safety"><div class="lh-audit-group__header"><span class="lh-audit-group__title">Trust and Safety</span></div><div class="lh-audit lh-audit--informative" id="csp-xss">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Ensure CSP is effective against XSS attacks</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>A strong Content Security Policy (CSP) significantly reduces the risk of cross-site scripting (XSS) attacks. <a rel="noopener" target="_blank" href="https://web.dev/strict-csp/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a></span></div>
<div class="lh-audit__stackpacks"></div>
<table class="lh-table lh-details"><thead><tr><th class="lh-table-column--text"><div class="lh-text">Description</div></th><th class="lh-table-column--code"><div class="lh-text">Directive</div></th><th class="lh-table-column--text"><div class="lh-text">Severity</div></th></tr></thead><tbody><tr class="lh-row--even"><td class="lh-table-column--text"><div class="lh-text">No CSP found in enforcement mode</div></td><td class="lh-table-column--empty"></td><td class="lh-table-column--text"><div class="lh-text">High</div></td></tr></tbody></table></details>
</div></div></div><details class="lh-clump lh-audit-group lh-clump--passed">
<summary>
<div class="lh-audit-group__summary">
<div class="lh-audit-group__header">
<span class="lh-audit-group__title">Passed audits</span>
<span class="lh-audit-group__itemcount">(17)</span>
<!-- .lh-audit-group__description will be added here -->
<!-- .lh-metrics-toggle will be added here -->
</div>
<div class=""></div>
<svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</summary>
<div class="lh-audit lh-audit--binary lh-audit--pass" id="is-on-https">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Uses HTTPS</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>All sites should be protected with HTTPS, even ones that don't handle sensitive data. This includes avoiding <a rel="noopener" target="_blank" href="https://developers.google.com/web/fundamentals/security/prevent-mixed-content/what-is-mixed-content?utm_source=lighthouse&amp;utm_medium=cli">mixed content</a>, where some resources are loaded over HTTP despite the initial request being served over HTTPS. HTTPS prevents intruders from tampering with or passively listening in on the communications between your app and your users, and is a prerequisite for HTTP/2 and many new web platform APIs. <a rel="noopener" target="_blank" href="https://web.dev/is-on-https/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="external-anchors-use-rel-noopener">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Links to cross-origin destinations are safe</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Add `rel="noopener"` or `rel="noreferrer"` to any external links to improve performance and prevent security vulnerabilities. <a rel="noopener" target="_blank" href="https://web.dev/external-anchors-use-rel-noopener/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="geolocation-on-start">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Avoids requesting the geolocation permission on page load</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Users are mistrustful of or confused by sites that request their location without context. Consider tying the request to a user action instead. <a rel="noopener" target="_blank" href="https://web.dev/geolocation-on-start/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="notification-on-start">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Avoids requesting the notification permission on page load</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Users are mistrustful of or confused by sites that request to send notifications without context. Consider tying the request to user gestures instead. <a rel="noopener" target="_blank" href="https://web.dev/notification-on-start/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="no-vulnerable-libraries">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Avoids front-end JavaScript libraries with known security vulnerabilities</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Some third-party scripts may contain known security vulnerabilities that are easily identified and exploited by attackers. <a rel="noopener" target="_blank" href="https://web.dev/no-vulnerable-libraries/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="password-inputs-can-be-pasted-into">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Allows users to paste into password fields</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Preventing password pasting undermines good security policy. <a rel="noopener" target="_blank" href="https://web.dev/password-inputs-can-be-pasted-into/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="image-aspect-ratio">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Displays images with correct aspect ratio</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Image display dimensions should match natural aspect ratio. <a rel="noopener" target="_blank" href="https://web.dev/image-aspect-ratio/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="image-size-responsive">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Serves images with appropriate resolution</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Image natural dimensions should be proportional to the display size and the pixel ratio to maximize image clarity. <a rel="noopener" target="_blank" href="https://web.dev/serve-responsive-images/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="doctype">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Page has the HTML doctype</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Specifying a doctype prevents the browser from switching to quirks-mode. <a rel="noopener" target="_blank" href="https://web.dev/doctype/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="charset">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Properly defines charset</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>A character encoding declaration is required. It can be done with a `&lt;meta&gt;` tag in the first 1024 bytes of the HTML or in the Content-Type HTTP response header. <a rel="noopener" target="_blank" href="https://web.dev/charset/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="no-unload-listeners">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Avoids <code>unload</code> event listeners</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>The `unload` event does not fire reliably and listening for it can prevent browser optimizations like the Back-Forward Cache. Consider using the `pagehide` or `visibilitychange` events instead. <a rel="noopener" target="_blank" href="https://developers.google.com/web/updates/2018/07/page-lifecycle-api?utm_source=lighthouse&amp;utm_medium=cli#the-unload-event">Learn more</a></span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="appcache-manifest">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Avoids Application Cache</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Application Cache is deprecated. <a rel="noopener" target="_blank" href="https://web.dev/appcache-manifest/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="js-libraries">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Detected JavaScript libraries</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>All front-end JavaScript libraries detected on the page. <a rel="noopener" target="_blank" href="https://web.dev/js-libraries/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<table class="lh-table lh-details"><thead><tr><th class="lh-table-column--text"><div class="lh-text">Name</div></th><th class="lh-table-column--text"><div class="lh-text">Version</div></th></tr></thead><tbody><tr class="lh-row--even"><td class="lh-table-column--text"><div class="lh-text">FuseJS</div></td><td class="lh-table-column--empty"></td></tr></tbody></table></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="deprecations">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Avoids deprecated APIs</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Deprecated APIs will eventually be removed from the browser. <a rel="noopener" target="_blank" href="https://web.dev/deprecations/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="errors-in-console">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>No browser errors logged to the console</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Errors logged to the console indicate unresolved problems. They can come from network request failures and other browser concerns. <a rel="noopener" target="_blank" href="https://web.dev/errors-in-console/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a></span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="valid-source-maps">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Page has valid source maps</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Source maps translate minified code to the original source code. This helps developers debug in production. In addition, Lighthouse is able to provide further insights. Consider deploying source maps to take advantage of these benefits. <a rel="noopener" target="_blank" href="https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="inspector-issues">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>No issues in the <code>Issues</code> panel in Chrome Devtools</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Issues logged to the `Issues` panel in Chrome Devtools indicate unresolved problems. They can come from network request failures, insufficient security controls, and other browser concerns. Open up the Issues panel in Chrome DevTools for more details on each issue.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div></details><details class="lh-clump lh-audit-group lh-clump--notapplicable">
<summary>
<div class="lh-audit-group__summary">
<div class="lh-audit-group__header">
<span class="lh-audit-group__title">Not applicable</span>
<span class="lh-audit-group__itemcount">(1)</span>
<!-- .lh-audit-group__description will be added here -->
<!-- .lh-metrics-toggle will be added here -->
</div>
<div class=""></div>
<svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</summary>
<div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="preload-fonts">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Fonts with <code>font-display: optional</code> are preloaded</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Preload `optional` fonts so first-time visitors may use them. <a rel="noopener" target="_blank" href="https://web.dev/preload-optional-fonts/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a></span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div></details></div></div><div class="lh-category-wrapper"><div class="lh-category"><span class="lh-permalink" id="seo"></span>
<div class="lh-category-header">
<div class="lh-score__gauge" role="heading" aria-level="2">
<a href="#seo" class="lh-gauge__wrapper lh-gauge__wrapper--pass">
<!-- Wrapper exists for the ::before plugin icon. Cannot create pseudo-elements on svgs. -->
<div class="lh-gauge__svg-wrapper">
<svg viewBox="0 0 120 120" class="lh-gauge">
<circle class="lh-gauge-base" r="56" cx="60" cy="60" stroke-width="8"></circle>
<circle class="lh-gauge-arc" r="56" cx="60" cy="60" stroke-width="8" style="transform: rotate(-87.95372216024705deg); stroke-dasharray: 351.85837720205683px, 351.85837720205683px;"></circle>
</svg>
</div>
<div class="lh-gauge__percentage">100</div>
<!-- TODO: should likely be an h2 -->
<div class="lh-gauge__label">SEO</div>
</a>
</div>
<div class="lh-category-header__description"><span>These checks ensure that your page is optimized for search engine results ranking. There are additional factors Lighthouse does not check that may affect your search ranking. <a rel="noopener" target="_blank" href="https://support.google.com/webmasters/answer/35769">Learn more</a>.</span></div>
</div>
<details class="lh-clump lh-audit-group lh-clump--manual">
<summary>
<div class="lh-audit-group__summary">
<div class="lh-audit-group__header">
<span class="lh-audit-group__title">Additional items to manually check</span>
<span class="lh-audit-group__itemcount">(1)</span>
<!-- .lh-audit-group__description will be added here -->
<!-- .lh-metrics-toggle will be added here -->
<span class="lh-audit-group__description">Run these additional validators on your site to check additional SEO best practices.</span></div>
<div class=""></div>
<svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</summary>
<div class="lh-audit lh-audit--manual lh-audit--pass" id="structured-data">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Structured data is valid</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Run the <a rel="noopener" target="_blank" href="https://search.google.com/structured-data/testing-tool/">Structured Data Testing Tool</a> and the <a rel="noopener" target="_blank" href="http://linter.structured-data.org/">Structured Data Linter</a> to validate structured data. <a rel="noopener" target="_blank" href="https://web.dev/structured-data/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div></details><details class="lh-clump lh-audit-group lh-clump--passed">
<summary>
<div class="lh-audit-group__summary">
<div class="lh-audit-group__header">
<span class="lh-audit-group__title">Passed audits</span>
<span class="lh-audit-group__itemcount">(13)</span>
<!-- .lh-audit-group__description will be added here -->
<!-- .lh-metrics-toggle will be added here -->
</div>
<div class=""></div>
<svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</summary>
<div class="lh-audit lh-audit--binary lh-audit--pass" id="viewport">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Has a <code>&lt;meta name="viewport"&gt;</code> tag with <code>width</code> or <code>initial-scale</code></span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Add a `&lt;meta name="viewport"&gt;` tag to optimize your app for mobile screens. <a rel="noopener" target="_blank" href="https://web.dev/viewport/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="document-title">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Document has a <code>&lt;title&gt;</code> element</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>The title gives screen reader users an overview of the page, and search engine users rely on it heavily to determine if a page is relevant to their search. <a rel="noopener" target="_blank" href="https://web.dev/document-title/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="meta-description">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Document has a meta description</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Meta descriptions may be included in search results to concisely summarize page content. <a rel="noopener" target="_blank" href="https://web.dev/meta-description/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="http-status-code">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Page has successful HTTP status code</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Pages with unsuccessful HTTP status codes may not be indexed properly. <a rel="noopener" target="_blank" href="https://web.dev/http-status-code/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="link-text">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Links have descriptive text</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Descriptive link text helps search engines understand your content. <a rel="noopener" target="_blank" href="https://web.dev/link-text/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="crawlable-anchors">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Links are crawlable</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Search engines may use `href` attributes on links to crawl websites. Ensure that the `href` attribute of anchor elements links to an appropriate destination, so more pages of the site can be discovered. <a rel="noopener" target="_blank" href="https://support.google.com/webmasters/answer/9112205">Learn More</a></span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="is-crawlable">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Page isnt blocked from indexing</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Search engines are unable to include your pages in search results if they don't have permission to crawl them. <a rel="noopener" target="_blank" href="https://web.dev/is-crawable/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="image-alt">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Image elements have <code>[alt]</code> attributes</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Informative elements should aim for short, descriptive alternate text. Decorative elements can be ignored with an empty alt attribute. <a rel="noopener" target="_blank" href="https://web.dev/image-alt/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="hreflang">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Document has a valid <code>hreflang</code></span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>hreflang links tell search engines what version of a page they should list in search results for a given language or region. <a rel="noopener" target="_blank" href="https://web.dev/hreflang/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="canonical">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Document has a valid <code>rel=canonical</code></span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Canonical links suggest which URL to show in search results. <a rel="noopener" target="_blank" href="https://web.dev/canonical/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="font-size">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Document uses legible font sizes</span></span>
<span class="lh-audit__display-text">99.75% legible text</span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Font sizes less than 12px are too small to be legible and require mobile visitors to “pinch to zoom” in order to read. Strive to have &gt;60% of page text ≥12px. <a rel="noopener" target="_blank" href="https://web.dev/font-size/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<div class="lh-3p-filter">
<label class="lh-3p-filter-label" for="lh-3p-filter-label--8">
<input type="checkbox" class="lh-3p-filter-input" checked="" id="lh-3p-filter-label--8" disabled="">
<span class="lh-3p-ui-string">Show 3rd-party resources</span> (<span class="lh-3p-filter-count">0</span>)
</label>
</div>
<table class="lh-table lh-details"><thead><tr><th class="lh-table-column--source-location"><div class="lh-text">Source</div></th><th class="lh-table-column--code"><div class="lh-text">Selector</div></th><th class="lh-table-column--text"><div class="lh-text">% of Page Text</div></th><th class="lh-table-column--text"><div class="lh-text">Font Size</div></th></tr></thead><tbody><tr class="lh-row--even"><td class="lh-table-column--source-location"><div class="lh-text__url lh-source-location" title="http://localhost:8008/congo/css/main.bundle.min.6783e342ffedbcc698d9afb9ffe11276f4f9de79bfc45cf670b35d5f9948d266086106596bc9e59820d7de0959e5889ce97e073fbafd380be9dc7b33b75295f2.css" data-url="http://localhost:8008/congo/css/main.bundle.min.6783e342ffedbcc698d9afb9ffe11276f4f9de79bfc45cf670b35d5f9948d266086106596bc9e59820d7de0959e5889ce97e073fbafd380be9dc7b33b75295f2.css" data-source-url="http://localhost:8008/congo/css/main.bundle.min.6783e342ffedbcc698d9afb9ffe11276f4f9de79bfc45cf670b35d5f9948d266086106596bc9e59820d7de0959e5889ce97e073fbafd380be9dc7b33b75295f2.css" data-source-line="0" data-source-column="28538"><a rel="noopener" target="_blank" href="http://localhost:8008/congo/css/main.bundle.min.6783e342ffedbcc698d9afb9ffe11276f4f9de79bfc45cf670b35d5f9948d266086106596bc9e59820d7de0959e5889ce97e073fbafd380be9dc7b33b75295f2.css" class="lh-link">…css/main.bundle.min.6783e34….css:1:28538</a><div class="lh-text lh-text__url-host">(localhost)</div></div></td><td class="lh-table-column--code"><pre class="lh-code">.text-\[0\.6rem\]</pre></td><td class="lh-table-column--text"><div class="lh-text">0.25%</div></td><td class="lh-table-column--text"><div class="lh-text">9.6px</div></td></tr><tr class="lh-row--odd"><td class="lh-table-column--source-location"><pre class="lh-code">Legible text</pre></td><td class="lh-table-column--code"><pre class="lh-code"></pre></td><td class="lh-table-column--text"><div class="lh-text">99.75%</div></td><td class="lh-table-column--text"><div class="lh-text">≥ 12px</div></td></tr></tbody></table></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="plugins">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Document avoids plugins</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Search engines can't index plugin content, and many devices restrict plugins or don't support them. <a rel="noopener" target="_blank" href="https://web.dev/plugins/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="tap-targets">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Tap targets are sized appropriately</span></span>
<span class="lh-audit__display-text">100% appropriately sized tap targets</span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Interactive elements like buttons and links should be large enough (48x48px), and have enough space around them, to be easy enough to tap without overlapping onto other elements. <a rel="noopener" target="_blank" href="https://web.dev/tap-targets/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div></details><details class="lh-clump lh-audit-group lh-clump--notapplicable">
<summary>
<div class="lh-audit-group__summary">
<div class="lh-audit-group__header">
<span class="lh-audit-group__title">Not applicable</span>
<span class="lh-audit-group__itemcount">(1)</span>
<!-- .lh-audit-group__description will be added here -->
<!-- .lh-metrics-toggle will be added here -->
</div>
<div class=""></div>
<svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</summary>
<div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="robots-txt">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>robots.txt is valid</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>If your robots.txt file is malformed, crawlers may not be able to understand how you want your website to be crawled or indexed. <a rel="noopener" target="_blank" href="https://web.dev/robots-txt/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div></details></div></div><div class="lh-category-wrapper"><div class="lh-category"><span class="lh-permalink" id="pwa"></span>
<div class="lh-category-header">
<div class="lh-score__gauge" role="heading" aria-level="2">
<a href="#pwa" class="lh-gauge__wrapper lh-gauge--pwa__wrapper lh-badged--pwa-installable" title="Installable: 1/1, PWA Optimized: 6/8">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" class="lh-gauge lh-gauge--pwa">
<defs>
<linearGradient id="lh-gauge--pwa__check-circle__gradient-2" x1="50%" y1="0%" x2="50%" y2="100%">
<stop stop-color="#00C852" offset="0%"></stop>
<stop stop-color="#009688" offset="100%"></stop>
</linearGradient>
<linearGradient id="lh-gauge--pwa__installable__shadow-gradient-2" x1="76.056%" x2="24.111%" y1="82.995%" y2="24.735%">
<stop stop-color="#A5D6A7" offset="0%"></stop>
<stop stop-color="#80CBC4" offset="100%"></stop>
</linearGradient>
<g id="lh-gauge--pwa__installable-badge-2">
<circle fill="#FFFFFF" cx="10" cy="10" r="10"></circle>
<path fill="#009688" d="M10 4.167A5.835 5.835 0 0 0 4.167 10 5.835 5.835 0 0 0 10 15.833 5.835 5.835 0 0 0 15.833 10 5.835 5.835 0 0 0 10 4.167zm2.917 6.416h-2.334v2.334H9.417v-2.334H7.083V9.417h2.334V7.083h1.166v2.334h2.334v1.166z"></path>
</g>
</defs>
<g stroke="none" fill-rule="nonzero">
<!-- Background and PWA logo (color by default) -->
<circle class="lh-gauge--pwa__disc" cx="30" cy="30" r="30"></circle>
<g class="lh-gauge--pwa__logo">
<path class="lh-gauge--pwa__logo--secondary-color" d="M35.66 19.39l.7-1.75h2L37.4 15 38.6 12l3.4 9h-2.51l-.58-1.61z"></path>
<path class="lh-gauge--pwa__logo--primary-color" d="M33.52 21l3.65-9h-2.42l-2.5 5.82L30.5 12h-1.86l-1.9 5.82-1.35-2.65-1.21 3.72L25.4 21h2.38l1.72-5.2 1.64 5.2z"></path>
<path class="lh-gauge--pwa__logo--secondary-color" fill-rule="nonzero" d="M20.3 17.91h1.48c.45 0 .85-.05 1.2-.15l.39-1.18 1.07-3.3a2.64 2.64 0 0 0-.28-.37c-.55-.6-1.36-.91-2.42-.91H18v9h2.3V17.9zm1.96-3.84c.22.22.33.5.33.87 0 .36-.1.65-.29.87-.2.23-.59.35-1.15.35h-.86v-2.41h.87c.52 0 .89.1 1.1.32z"></path>
</g>
<!-- No badges. -->
<rect class="lh-gauge--pwa__component lh-gauge--pwa__na-line" fill="#FFFFFF" x="20" y="32" width="20" height="4" rx="2"></rect>
<!-- Just installable. -->
<g class="lh-gauge--pwa__component lh-gauge--pwa__installable-badge" transform="translate(20, 29)">
<path fill="url(#lh-gauge--pwa__installable__shadow-gradient-2)" d="M33.629 19.487c-4.272 5.453-10.391 9.39-17.415 10.869L3 17.142 17.142 3 33.63 19.487z"></path>
<use href="#lh-gauge--pwa__installable-badge-2"></use>
</g>
<!-- Full PWA. -->
<g class="lh-gauge--pwa__component lh-gauge--pwa__check-circle" transform="translate(18, 28)">
<circle fill="#FFFFFF" cx="12" cy="12" r="12"></circle>
<path fill="url(#lh-gauge--pwa__check-circle__gradient-2)" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"></path>
</g>
</g>
</svg>
<div class="lh-gauge__label">Progressive Web App</div>
</a>
</div>
<div class="lh-category-header__description"><span>These checks validate the aspects of a Progressive Web App. <a rel="noopener" target="_blank" href="https://developers.google.com/web/progressive-web-apps/checklist?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
</div>
<div><div class="lh-audit-group lh-audit-group--pwa-installable lh-badged"><div class="lh-audit-group__header"><span class="lh-audit-group__title">Installable</span></div><div class="lh-audit lh-audit--binary lh-audit--pass" id="installable-manifest">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Web app manifest and service worker meet the installability requirements</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Service worker is the technology that enables your app to use many Progressive Web App features, such as offline, add to homescreen, and push notifications. With proper service worker and manifest implementations, browsers can proactively prompt users to add your app to their homescreen, which can lead to higher engagement. <a rel="noopener" target="_blank" href="https://web.dev/installable-manifest/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
<span class="lh-details"></span></details>
</div></div><div class="lh-audit-group lh-audit-group--pwa-optimized"><div class="lh-audit-group__header"><span class="lh-audit-group__title">PWA Optimized</span></div><div class="lh-audit lh-audit--binary lh-audit--fail" id="service-worker">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Does not register a service worker that controls page and <code>start_url</code></span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>The service worker is the technology that enables your app to use many Progressive Web App features, such as offline, add to homescreen, and push notifications. <a rel="noopener" target="_blank" href="https://web.dev/service-worker/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--notapplicable lh-audit--pass" id="redirects-http">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Redirects HTTP traffic to HTTPS</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>If you've already set up HTTPS, make sure that you redirect all HTTP traffic to HTTPS in order to enable secure web features for all your users. <a rel="noopener" target="_blank" href="https://web.dev/redirects-http/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="splash-screen">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Configured for a custom splash screen</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>A themed splash screen ensures a high-quality experience when users launch your app from their homescreens. <a rel="noopener" target="_blank" href="https://web.dev/splash-screen/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--binary lh-audit--fail" id="themed-omnibox">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Does not set a theme color for the address bar.</span><div class="lh-audit-explanation">Failures: No `&lt;meta name="theme-color"&gt;` tag found.</div></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>The browser address bar can be themed to match your site. <a rel="noopener" target="_blank" href="https://web.dev/themed-omnibox/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="content-width">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Content is sized correctly for the viewport</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>If the width of your app's content doesn't match the width of the viewport, your app might not be optimized for mobile screens. <a rel="noopener" target="_blank" href="https://web.dev/content-width/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="viewport">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Has a <code>&lt;meta name="viewport"&gt;</code> tag with <code>width</code> or <code>initial-scale</code></span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Add a `&lt;meta name="viewport"&gt;` tag to optimize your app for mobile screens. <a rel="noopener" target="_blank" href="https://web.dev/viewport/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="apple-touch-icon">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Provides a valid <code>apple-touch-icon</code></span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>For ideal appearance on iOS when users add a progressive web app to the home screen, define an `apple-touch-icon`. It must point to a non-transparent 192px (or 180px) square PNG. <a rel="noopener" target="_blank" href="https://web.dev/apple-touch-icon/?utm_source=lighthouse&amp;utm_medium=cli">Learn More</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--binary lh-audit--pass" id="maskable-icon">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Manifest has a maskable icon</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>A maskable icon ensures that the image fills the entire shape without being letterboxed when installing the app on a device. <a rel="noopener" target="_blank" href="https://web.dev/maskable-icon-audit/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div></div></div><details class="lh-clump lh-audit-group lh-clump--manual">
<summary>
<div class="lh-audit-group__summary">
<div class="lh-audit-group__header">
<span class="lh-audit-group__title">Additional items to manually check</span>
<span class="lh-audit-group__itemcount">(3)</span>
<!-- .lh-audit-group__description will be added here -->
<!-- .lh-metrics-toggle will be added here -->
<span class="lh-audit-group__description">These checks are required by the baseline <a rel="noopener" target="_blank" href="https://developers.google.com/web/progressive-web-apps/checklist?utm_source=lighthouse&amp;utm_medium=cli">PWA Checklist</a> but are not automatically checked by Lighthouse. They do not affect your score but it's important that you verify them manually.</span></div>
<div class=""></div>
<svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</summary>
<div class="lh-audit lh-audit--manual lh-audit--pass" id="pwa-cross-browser">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Site works cross-browser</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>To reach the most number of users, sites should work across every major browser. <a rel="noopener" target="_blank" href="https://web.dev/pwa-cross-browser/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--manual lh-audit--pass" id="pwa-page-transitions">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Page transitions don't feel like they block on the network</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Transitions should feel snappy as you tap around, even on a slow network. This experience is key to a user's perception of performance. <a rel="noopener" target="_blank" href="https://web.dev/pwa-page-transitions/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div><div class="lh-audit lh-audit--manual lh-audit--pass" id="pwa-each-page-has-url">
<details class="lh-expandable-details">
<summary>
<div class="lh-audit__header lh-expandable-details__summary">
<span class="lh-audit__score-icon"></span>
<span class="lh-audit__title-and-text">
<span class="lh-audit__title"><span>Each page has a URL</span></span>
<span class="lh-audit__display-text"></span>
</span>
<div class="lh-chevron-container"><svg class="lh-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g class="lh-chevron__lines">
<path class="lh-chevron__line lh-chevron__line-left" d="M10 50h40"></path>
<path class="lh-chevron__line lh-chevron__line-right" d="M90 50H50"></path>
</g>
</svg></div>
</div>
</summary>
<div class="lh-audit__description"><span>Ensure individual pages are deep linkable via URL and that URLs are unique for the purpose of shareability on social media. <a rel="noopener" target="_blank" href="https://web.dev/pwa-each-page-has-url/?utm_source=lighthouse&amp;utm_medium=cli">Learn more</a>.</span></div>
<div class="lh-audit__stackpacks"></div>
</details>
</div></details></div></div></div>
<style>
.lh-footer {
padding: var(--footer-padding-vertical) calc(var(--default-padding) * 2);
max-width: var(--report-width);
margin: 0 auto;
}
.lh-footer .lh-generated {
text-align: center;
}
.lh-env__title {
font-size: var(--env-item-font-size-big);
line-height: var(--env-item-line-height-big);
text-align: center;
padding: var(--score-container-padding);
}
.lh-env {
padding: var(--default-padding) 0;
}
.lh-env__items {
padding-left: 16px;
margin: 0 0 var(--audits-margin-bottom);
padding: 0;
}
.lh-env__items .lh-env__item:nth-child(2n) {
background-color: var(--env-item-background-color);
}
.lh-env__item {
display: flex;
padding: var(--env-item-padding);
position: relative;
}
span.lh-env__name {
font-weight: bold;
min-width: var(--env-name-min-width);
flex: 0.5;
padding: 0 8px;
}
span.lh-env__description {
text-align: left;
flex: 1;
}
</style>
<footer class="lh-footer">
<!-- TODO(i18n): localize runtime settings -->
<div class="lh-env">
<div class="lh-env__title">Runtime Settings</div>
<ul class="lh-env__items" id="runtime-settings">
<template id="tmpl-lh-env__items">
<li class="lh-env__item">
<span class="lh-env__name"></span>
<span class="lh-env__description"></span>
</li>
</template>
<li class="lh-env__item">
<span class="lh-env__name">URL</span>
<span class="lh-env__description">http://localhost:8008/congo/samples/emoji/</span>
</li>
<li class="lh-env__item">
<span class="lh-env__name">Fetch Time</span>
<span class="lh-env__description">Jan 19, 2022, 7:17 PM GMT+11</span>
</li>
<li class="lh-env__item">
<span class="lh-env__name">Device</span>
<span class="lh-env__description">Emulated Moto G4</span>
</li>
<li class="lh-env__item">
<span class="lh-env__name">Network throttling</span>
<span class="lh-env__description">150&nbsp;ms TCP RTT, 1,638.4&nbsp;Kbps throughput (Simulated)</span>
</li>
<li class="lh-env__item">
<span class="lh-env__name">CPU throttling</span>
<span class="lh-env__description">4x slowdown (Simulated)</span>
</li>
<li class="lh-env__item">
<span class="lh-env__name">Channel</span>
<span class="lh-env__description">cli</span>
</li>
<li class="lh-env__item">
<span class="lh-env__name">User agent (host)</span>
<span class="lh-env__description">Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/96.0.4664.110 Safari/537.36</span>
</li>
<li class="lh-env__item">
<span class="lh-env__name">User agent (network)</span>
<span class="lh-env__description">Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4420.0 Mobile Safari/537.36 Chrome-Lighthouse</span>
</li>
<li class="lh-env__item">
<span class="lh-env__name">CPU/Memory Power</span>
<span class="lh-env__description">1949</span>
</li>
<li class="lh-env__item">
<span class="lh-env__name">Axe version</span>
<span class="lh-env__description">4.2.1</span>
</li>
</ul>
</div>
<div class="lh-generated">
<!-- TODO(i18n): use ICU replacement to replace version w/o concatenation. -->
Generated by <b>Lighthouse</b> <span class="lh-footer__version">8.0.0</span> |
<a href="https://github.com/GoogleChrome/Lighthouse/issues" target="_blank" rel="noopener" class="lh-footer__version_issue">File an issue</a>
</div>
</footer>
</div></div></main>
<div id="lh-log"></div>
<script>/**
* @license
* Copyright 2017 The Lighthouse Authors. All Rights Reserved.
*
* 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
*
* 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.
*/
'use strict';
/* globals self */
/** @template T @typedef {import('./i18n')<T>} I18n */
const ELLIPSIS = '\u2026';
const NBSP = '\xa0';
const PASS_THRESHOLD = 0.9;
const SCREENSHOT_PREFIX = 'data:image/jpeg;base64,';
const RATINGS = {
PASS: {label: 'pass', minScore: PASS_THRESHOLD},
AVERAGE: {label: 'average', minScore: 0.5},
FAIL: {label: 'fail'},
ERROR: {label: 'error'},
};
// 25 most used tld plus one domains (aka public suffixes) from http archive.
// @see https://github.com/GoogleChrome/lighthouse/pull/5065#discussion_r191926212
// The canonical list is https://publicsuffix.org/learn/ but we're only using subset to conserve bytes
const listOfTlds = [
'com', 'co', 'gov', 'edu', 'ac', 'org', 'go', 'gob', 'or', 'net', 'in', 'ne', 'nic', 'gouv',
'web', 'spb', 'blog', 'jus', 'kiev', 'mil', 'wi', 'qc', 'ca', 'bel', 'on',
];
class Util {
static get PASS_THRESHOLD() {
return PASS_THRESHOLD;
}
static get MS_DISPLAY_VALUE() {
return `%10d${NBSP}ms`;
}
/**
* Returns a new LHR that's reshaped for slightly better ergonomics within the report rendereer.
* Also, sets up the localized UI strings used within renderer and makes changes to old LHRs to be
* compatible with current renderer.
* The LHR passed in is not mutated.
* TODO(team): we all agree the LHR shape change is technical debt we should fix
* @param {LH.Result} result
* @return {LH.ReportResult}
*/
static prepareReportResult(result) {
// If any mutations happen to the report within the renderers, we want the original object untouched
const clone = /** @type {LH.ReportResult} */ (JSON.parse(JSON.stringify(result)));
// If LHR is older (≤3.0.3), it has no locale setting. Set default.
if (!clone.configSettings.locale) {
clone.configSettings.locale = 'en';
}
if (!clone.configSettings.formFactor) {
// @ts-expect-error fallback handling for emulatedFormFactor
clone.configSettings.formFactor = clone.configSettings.emulatedFormFactor;
}
for (const audit of Object.values(clone.audits)) {
// Turn 'not-applicable' (LHR <4.0) and 'not_applicable' (older proto versions)
// into 'notApplicable' (LHR ≥4.0).
// @ts-expect-error tsc rightly flags that these values shouldn't occur.
// eslint-disable-next-line max-len
if (audit.scoreDisplayMode === 'not_applicable' || audit.scoreDisplayMode === 'not-applicable') {
audit.scoreDisplayMode = 'notApplicable';
}
if (audit.details) {
// Turn `auditDetails.type` of undefined (LHR <4.2) and 'diagnostic' (LHR <5.0)
// into 'debugdata' (LHR ≥5.0).
// @ts-expect-error tsc rightly flags that these values shouldn't occur.
if (audit.details.type === undefined || audit.details.type === 'diagnostic') {
// @ts-expect-error details is of type never.
audit.details.type = 'debugdata';
}
// Add the jpg data URL prefix to filmstrip screenshots without them (LHR <5.0).
if (audit.details.type === 'filmstrip') {
for (const screenshot of audit.details.items) {
if (!screenshot.data.startsWith(SCREENSHOT_PREFIX)) {
screenshot.data = SCREENSHOT_PREFIX + screenshot.data;
}
}
}
}
}
// For convenience, smoosh all AuditResults into their auditRef (which has just weight & group)
if (typeof clone.categories !== 'object') throw new Error('No categories provided.');
/** @type {Map<string, Array<LH.ReportResult.AuditRef>>} */
const relevantAuditToMetricsMap = new Map();
for (const category of Object.values(clone.categories)) {
// Make basic lookup table for relevantAudits
category.auditRefs.forEach(metricRef => {
if (!metricRef.relevantAudits) return;
metricRef.relevantAudits.forEach(auditId => {
const arr = relevantAuditToMetricsMap.get(auditId) || [];
arr.push(metricRef);
relevantAuditToMetricsMap.set(auditId, arr);
});
});
category.auditRefs.forEach(auditRef => {
const result = clone.audits[auditRef.id];
auditRef.result = result;
// Attach any relevantMetric auditRefs
if (relevantAuditToMetricsMap.has(auditRef.id)) {
auditRef.relevantMetrics = relevantAuditToMetricsMap.get(auditRef.id);
}
// attach the stackpacks to the auditRef object
if (clone.stackPacks) {
clone.stackPacks.forEach(pack => {
if (pack.descriptions[auditRef.id]) {
auditRef.stackPacks = auditRef.stackPacks || [];
auditRef.stackPacks.push({
title: pack.title,
iconDataURL: pack.iconDataURL,
description: pack.descriptions[auditRef.id],
});
}
});
}
});
}
return clone;
}
/**
* Used to determine if the "passed" for the purposes of showing up in the "failed" or "passed"
* sections of the report.
*
* @param {{score: (number|null), scoreDisplayMode: string}} audit
* @return {boolean}
*/
static showAsPassed(audit) {
switch (audit.scoreDisplayMode) {
case 'manual':
case 'notApplicable':
return true;
case 'error':
case 'informative':
return false;
case 'numeric':
case 'binary':
default:
return Number(audit.score) >= RATINGS.PASS.minScore;
}
}
/**
* Convert a score to a rating label.
* @param {number|null} score
* @param {string=} scoreDisplayMode
* @return {string}
*/
static calculateRating(score, scoreDisplayMode) {
// Handle edge cases first, manual and not applicable receive 'pass', errored audits receive 'error'
if (scoreDisplayMode === 'manual' || scoreDisplayMode === 'notApplicable') {
return RATINGS.PASS.label;
} else if (scoreDisplayMode === 'error') {
return RATINGS.ERROR.label;
} else if (score === null) {
return RATINGS.FAIL.label;
}
// At this point, we're rating a standard binary/numeric audit
let rating = RATINGS.FAIL.label;
if (score >= RATINGS.PASS.minScore) {
rating = RATINGS.PASS.label;
} else if (score >= RATINGS.AVERAGE.minScore) {
rating = RATINGS.AVERAGE.label;
}
return rating;
}
/**
* Split a string by markdown code spans (enclosed in `backticks`), splitting
* into segments that were enclosed in backticks (marked as `isCode === true`)
* and those that outside the backticks (`isCode === false`).
* @param {string} text
* @return {Array<{isCode: true, text: string}|{isCode: false, text: string}>}
*/
static splitMarkdownCodeSpans(text) {
/** @type {Array<{isCode: true, text: string}|{isCode: false, text: string}>} */
const segments = [];
// Split on backticked code spans.
const parts = text.split(/`(.*?)`/g);
for (let i = 0; i < parts.length; i ++) {
const text = parts[i];
// Empty strings are an artifact of splitting, not meaningful.
if (!text) continue;
// Alternates between plain text and code segments.
const isCode = i % 2 !== 0;
segments.push({
isCode,
text,
});
}
return segments;
}
/**
* Split a string on markdown links (e.g. [some link](https://...)) into
* segments of plain text that weren't part of a link (marked as
* `isLink === false`), and segments with text content and a URL that did make
* up a link (marked as `isLink === true`).
* @param {string} text
* @return {Array<{isLink: true, text: string, linkHref: string}|{isLink: false, text: string}>}
*/
static splitMarkdownLink(text) {
/** @type {Array<{isLink: true, text: string, linkHref: string}|{isLink: false, text: string}>} */
const segments = [];
const parts = text.split(/\[([^\]]+?)\]\((https?:\/\/.*?)\)/g);
while (parts.length) {
// Shift off the same number of elements as the pre-split and capture groups.
const [preambleText, linkText, linkHref] = parts.splice(0, 3);
if (preambleText) { // Skip empty text as it's an artifact of splitting, not meaningful.
segments.push({
isLink: false,
text: preambleText,
});
}
// Append link if there are any.
if (linkText && linkHref) {
segments.push({
isLink: true,
text: linkText,
linkHref,
});
}
}
return segments;
}
/**
* @param {URL} parsedUrl
* @param {{numPathParts?: number, preserveQuery?: boolean, preserveHost?: boolean}=} options
* @return {string}
*/
static getURLDisplayName(parsedUrl, options) {
// Closure optional properties aren't optional in tsc, so fallback needs undefined values.
options = options || {numPathParts: undefined, preserveQuery: undefined,
preserveHost: undefined};
const numPathParts = options.numPathParts !== undefined ? options.numPathParts : 2;
const preserveQuery = options.preserveQuery !== undefined ? options.preserveQuery : true;
const preserveHost = options.preserveHost || false;
let name;
if (parsedUrl.protocol === 'about:' || parsedUrl.protocol === 'data:') {
// Handle 'about:*' and 'data:*' URLs specially since they have no path.
name = parsedUrl.href;
} else {
name = parsedUrl.pathname;
const parts = name.split('/').filter(part => part.length);
if (numPathParts && parts.length > numPathParts) {
name = ELLIPSIS + parts.slice(-1 * numPathParts).join('/');
}
if (preserveHost) {
name = `${parsedUrl.host}/${name.replace(/^\//, '')}`;
}
if (preserveQuery) {
name = `${name}${parsedUrl.search}`;
}
}
const MAX_LENGTH = 64;
// Always elide hexadecimal hash
name = name.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g, `$1${ELLIPSIS}`);
// Also elide other hash-like mixed-case strings
name = name.replace(/([a-zA-Z0-9-_]{9})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9-_]{10,}/g,
`$1${ELLIPSIS}`);
// Also elide long number sequences
name = name.replace(/(\d{3})\d{6,}/g, `$1${ELLIPSIS}`);
// Merge any adjacent ellipses
name = name.replace(/\u2026+/g, ELLIPSIS);
// Elide query params first
if (name.length > MAX_LENGTH && name.includes('?')) {
// Try to leave the first query parameter intact
name = name.replace(/\?([^=]*)(=)?.*/, `?$1$2${ELLIPSIS}`);
// Remove it all if it's still too long
if (name.length > MAX_LENGTH) {
name = name.replace(/\?.*/, `?${ELLIPSIS}`);
}
}
// Elide too long names next
if (name.length > MAX_LENGTH) {
const dotIndex = name.lastIndexOf('.');
if (dotIndex >= 0) {
name = name.slice(0, MAX_LENGTH - 1 - (name.length - dotIndex)) +
// Show file extension
`${ELLIPSIS}${name.slice(dotIndex)}`;
} else {
name = name.slice(0, MAX_LENGTH - 1) + ELLIPSIS;
}
}
return name;
}
/**
* Split a URL into a file, hostname and origin for easy display.
* @param {string} url
* @return {{file: string, hostname: string, origin: string}}
*/
static parseURL(url) {
const parsedUrl = new URL(url);
return {
file: Util.getURLDisplayName(parsedUrl),
hostname: parsedUrl.hostname,
origin: parsedUrl.origin,
};
}
/**
* @param {string|URL} value
* @return {!URL}
*/
static createOrReturnURL(value) {
if (value instanceof URL) {
return value;
}
return new URL(value);
}
/**
* Gets the tld of a domain
*
* @param {string} hostname
* @return {string} tld
*/
static getTld(hostname) {
const tlds = hostname.split('.').slice(-2);
if (!listOfTlds.includes(tlds[0])) {
return `.${tlds[tlds.length - 1]}`;
}
return `.${tlds.join('.')}`;
}
/**
* Returns a primary domain for provided hostname (e.g. www.example.com -> example.com).
* @param {string|URL} url hostname or URL object
* @returns {string}
*/
static getRootDomain(url) {
const hostname = Util.createOrReturnURL(url).hostname;
const tld = Util.getTld(hostname);
// tld is .com or .co.uk which means we means that length is 1 to big
// .com => 2 & .co.uk => 3
const splitTld = tld.split('.');
// get TLD + root domain
return hostname.split('.').slice(-splitTld.length).join('.');
}
/**
* @param {LH.Config.Settings} settings
* @return {!Array<{name: string, description: string}>}
*/
static getEnvironmentDisplayValues(settings) {
const emulationDesc = Util.getEmulationDescriptions(settings);
return [
{
name: Util.i18n.strings.runtimeSettingsDevice,
description: emulationDesc.deviceEmulation,
},
{
name: Util.i18n.strings.runtimeSettingsNetworkThrottling,
description: emulationDesc.networkThrottling,
},
{
name: Util.i18n.strings.runtimeSettingsCPUThrottling,
description: emulationDesc.cpuThrottling,
},
];
}
/**
* @param {LH.Config.Settings} settings
* @return {{deviceEmulation: string, networkThrottling: string, cpuThrottling: string}}
*/
static getEmulationDescriptions(settings) {
let cpuThrottling;
let networkThrottling;
const throttling = settings.throttling;
switch (settings.throttlingMethod) {
case 'provided':
cpuThrottling = Util.i18n.strings.throttlingProvided;
networkThrottling = Util.i18n.strings.throttlingProvided;
break;
case 'devtools': {
const {cpuSlowdownMultiplier, requestLatencyMs} = throttling;
cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (DevTools)`;
networkThrottling = `${Util.i18n.formatNumber(requestLatencyMs)}${NBSP}ms HTTP RTT, ` +
`${Util.i18n.formatNumber(throttling.downloadThroughputKbps)}${NBSP}Kbps down, ` +
`${Util.i18n.formatNumber(throttling.uploadThroughputKbps)}${NBSP}Kbps up (DevTools)`;
break;
}
case 'simulate': {
const {cpuSlowdownMultiplier, rttMs, throughputKbps} = throttling;
cpuThrottling = `${Util.i18n.formatNumber(cpuSlowdownMultiplier)}x slowdown (Simulated)`;
networkThrottling = `${Util.i18n.formatNumber(rttMs)}${NBSP}ms TCP RTT, ` +
`${Util.i18n.formatNumber(throughputKbps)}${NBSP}Kbps throughput (Simulated)`;
break;
}
default:
cpuThrottling = Util.i18n.strings.runtimeUnknown;
networkThrottling = Util.i18n.strings.runtimeUnknown;
}
// TODO(paulirish): revise Runtime Settings strings: https://github.com/GoogleChrome/lighthouse/pull/11796
const deviceEmulation = {
mobile: Util.i18n.strings.runtimeMobileEmulation,
desktop: Util.i18n.strings.runtimeDesktopEmulation,
}[settings.formFactor] || Util.i18n.strings.runtimeNoEmulation;
return {
deviceEmulation,
cpuThrottling,
networkThrottling,
};
}
/**
* Returns only lines that are near a message, or the first few lines if there are
* no line messages.
* @param {LH.Audit.Details.SnippetValue['lines']} lines
* @param {LH.Audit.Details.SnippetValue['lineMessages']} lineMessages
* @param {number} surroundingLineCount Number of lines to include before and after
* the message. If this is e.g. 2 this function might return 5 lines.
*/
static filterRelevantLines(lines, lineMessages, surroundingLineCount) {
if (lineMessages.length === 0) {
// no lines with messages, just return the first bunch of lines
return lines.slice(0, surroundingLineCount * 2 + 1);
}
const minGapSize = 3;
const lineNumbersToKeep = new Set();
// Sort messages so we can check lineNumbersToKeep to see how big the gap to
// the previous line is.
lineMessages = lineMessages.sort((a, b) => (a.lineNumber || 0) - (b.lineNumber || 0));
lineMessages.forEach(({lineNumber}) => {
let firstSurroundingLineNumber = lineNumber - surroundingLineCount;
let lastSurroundingLineNumber = lineNumber + surroundingLineCount;
while (firstSurroundingLineNumber < 1) {
// make sure we still show (surroundingLineCount * 2 + 1) lines in total
firstSurroundingLineNumber++;
lastSurroundingLineNumber++;
}
// If only a few lines would be omitted normally then we prefer to include
// extra lines to avoid the tiny gap
if (lineNumbersToKeep.has(firstSurroundingLineNumber - minGapSize - 1)) {
firstSurroundingLineNumber -= minGapSize;
}
for (let i = firstSurroundingLineNumber; i <= lastSurroundingLineNumber; i++) {
const surroundingLineNumber = i;
lineNumbersToKeep.add(surroundingLineNumber);
}
});
return lines.filter(line => lineNumbersToKeep.has(line.lineNumber));
}
/**
* @param {string} categoryId
*/
static isPluginCategory(categoryId) {
return categoryId.startsWith('lighthouse-plugin-');
}
}
/**
* Some parts of the report renderer require data found on the LHR. Instead of wiring it
* through, we have this global.
* @type {LH.ReportResult | null}
*/
Util.reportJson = null;
/**
* An always-increasing counter for making unique SVG ID suffixes.
*/
Util.getUniqueSuffix = (() => {
let svgSuffix = 0;
return function() {
return svgSuffix++;
};
})();
/** @type {I18n<typeof Util['UIStrings']>} */
// @ts-expect-error: Is set in report renderer.
Util.i18n = null;
/**
* Report-renderer-specific strings.
*/
Util.UIStrings = {
/** Disclaimer shown to users below the metric values (First Contentful Paint, Time to Interactive, etc) to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. */
varianceDisclaimer: 'Values are estimated and may vary. The [performance score is calculated](https://web.dev/performance-scoring/) directly from these metrics.',
/** Text link pointing to an interactive calculator that explains Lighthouse scoring. The link text should be fairly short. */
calculatorLink: 'See calculator.',
/** Label preceding a radio control for filtering the list of audits. The radio choices are various performance metrics (FCP, LCP, TBT), and if chosen, the audits in the report are hidden if they are not relevant to the selected metric. */
showRelevantAudits: 'Show audits relevant to:',
/** Column heading label for the listing of opportunity audits. Each audit title represents an opportunity. There are only 2 columns, so no strict character limit. */
opportunityResourceColumnLabel: 'Opportunity',
/** Column heading label for the estimated page load savings of opportunity audits. Estimated Savings is the total amount of time (in seconds) that Lighthouse computed could be reduced from the total page load time, if the suggested action is taken. There are only 2 columns, so no strict character limit. */
opportunitySavingsColumnLabel: 'Estimated Savings',
/** An error string displayed next to a particular audit when it has errored, but not provided any specific error message. */
errorMissingAuditInfo: 'Report error: no audit information',
/** A label, shown next to an audit title or metric title, indicating that there was an error computing it. The user can hover on the label to reveal a tooltip with the extended error message. Translation should be short (< 20 characters). */
errorLabel: 'Error!',
/** This label is shown above a bulleted list of warnings. It is shown directly below an audit that produced warnings. Warnings describe situations the user should be aware of, as Lighthouse was unable to complete all the work required on this audit. For example, The 'Unable to decode image (biglogo.jpg)' warning may show up below an image encoding audit. */
warningHeader: 'Warnings: ',
/** Section heading shown above a list of passed audits that contain warnings. Audits under this section do not negatively impact the score, but Lighthouse has generated some potentially actionable suggestions that should be reviewed. This section is expanded by default and displays after the failing audits. */
warningAuditsGroupTitle: 'Passed audits but with warnings',
/** Section heading shown above a list of audits that are passing. 'Passed' here refers to a passing grade. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */
passedAuditsGroupTitle: 'Passed audits',
/** Section heading shown above a list of audits that do not apply to the page. For example, if an audit is 'Are images optimized?', but the page has no images on it, the audit will be marked as not applicable. This is neither passing or failing. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */
notApplicableAuditsGroupTitle: 'Not applicable',
/** Section heading shown above a list of audits that were not computed by Lighthouse. They serve as a list of suggestions for the user to go and manually check. For example, Lighthouse can't automate testing cross-browser compatibility, so that is listed within this section, so the user is reminded to test it themselves. This section is collapsed by default, as the user should be focusing on the failed audits instead. Users can click this heading to reveal the list. */
manualAuditsGroupTitle: 'Additional items to manually check',
/** Label shown preceding any important warnings that may have invalidated the entire report. For example, if the user has Chrome extensions installed, they may add enough performance overhead that Lighthouse's performance metrics are unreliable. If shown, this will be displayed at the top of the report UI. */
toplevelWarningsMessage: 'There were issues affecting this run of Lighthouse:',
/** String of text shown in a graphical representation of the flow of network requests for the web page. This label represents the initial network request that fetches an HTML page. This navigation may be redirected (eg. Initial navigation to http://example.com redirects to https://www.example.com). */
crcInitialNavigation: 'Initial Navigation',
/** Label of value shown in the summary of critical request chains. Refers to the total amount of time (milliseconds) of the longest critical path chain/sequence of network requests. Example value: 2310 ms */
crcLongestDurationLabel: 'Maximum critical path latency:',
/** Label for button that shows all lines of the snippet when clicked */
snippetExpandButtonLabel: 'Expand snippet',
/** Label for button that only shows a few lines of the snippet when clicked */
snippetCollapseButtonLabel: 'Collapse snippet',
/** Explanation shown to users below performance results to inform them that the test was done with a 4G network connection and to warn them that the numbers they see will likely change slightly the next time they run Lighthouse. 'Lighthouse' becomes link text to additional documentation. */
lsPerformanceCategoryDescription: '[Lighthouse](https://developers.google.com/web/tools/lighthouse/) analysis of the current page on an emulated mobile network. Values are estimated and may vary.',
/** Title of the lab data section of the Performance category. Within this section are various speed metrics which quantify the pageload performance into values presented in seconds and milliseconds. "Lab" is an abbreviated form of "laboratory", and refers to the fact that the data is from a controlled test of a website, not measurements from real users visiting that site. */
labDataTitle: 'Lab Data',
/** This label is for a checkbox above a table of items loaded by a web page. The checkbox is used to show or hide third-party (or "3rd-party") resources in the table, where "third-party resources" refers to items loaded by a web page from URLs that aren't controlled by the owner of the web page. */
thirdPartyResourcesLabel: 'Show 3rd-party resources',
/** This label is for a button that opens a new tab to a webapp called "Treemap", which is a nested visual representation of a heierarchy of data releated to the reports (script bytes and coverage, resource breakdown, etc.) */
viewTreemapLabel: 'View Treemap',
/** Option in a dropdown menu that opens a small, summary report in a print dialog. */
dropdownPrintSummary: 'Print Summary',
/** Option in a dropdown menu that opens a full Lighthouse report in a print dialog. */
dropdownPrintExpanded: 'Print Expanded',
/** Option in a dropdown menu that copies the Lighthouse JSON object to the system clipboard. */
dropdownCopyJSON: 'Copy JSON',
/** Option in a dropdown menu that saves the Lighthouse report HTML locally to the system as a '.html' file. */
dropdownSaveHTML: 'Save as HTML',
/** Option in a dropdown menu that saves the Lighthouse JSON object to the local system as a '.json' file. */
dropdownSaveJSON: 'Save as JSON',
/** Option in a dropdown menu that opens the current report in the Lighthouse Viewer Application. */
dropdownViewer: 'Open in Viewer',
/** Option in a dropdown menu that saves the current report as a new Github Gist. */
dropdownSaveGist: 'Save as Gist',
/** Option in a dropdown menu that toggles the themeing of the report between Light(default) and Dark themes. */
dropdownDarkTheme: 'Toggle Dark Theme',
/** Title of the Runtime settings table in a Lighthouse report. Runtime settings are the environment configurations that a specific report used at auditing time. */
runtimeSettingsTitle: 'Runtime Settings',
/** Label for a row in a table that shows the URL that was audited during a Lighthouse run. */
runtimeSettingsUrl: 'URL',
/** Label for a row in a table that shows the time at which a Lighthouse run was conducted; formatted as a timestamp, e.g. Jan 1, 1970 12:00 AM UTC. */
runtimeSettingsFetchTime: 'Fetch Time',
/** Label for a row in a table that describes the kind of device that was emulated for the Lighthouse run. Example values for row elements: 'No Emulation', 'Emulated Desktop', etc. */
runtimeSettingsDevice: 'Device',
/** Label for a row in a table that describes the network throttling conditions that were used during a Lighthouse run, if any. */
runtimeSettingsNetworkThrottling: 'Network throttling',
/** Label for a row in a table that describes the CPU throttling conditions that were used during a Lighthouse run, if any.*/
runtimeSettingsCPUThrottling: 'CPU throttling',
/** Label for a row in a table that shows in what tool Lighthouse is being run (e.g. The lighthouse CLI, Chrome DevTools, Lightrider, WebPageTest, etc). */
runtimeSettingsChannel: 'Channel',
/** Label for a row in a table that shows the User Agent that was detected on the Host machine that ran Lighthouse. */
runtimeSettingsUA: 'User agent (host)',
/** Label for a row in a table that shows the User Agent that was used to send out all network requests during the Lighthouse run. */
runtimeSettingsUANetwork: 'User agent (network)',
/** Label for a row in a table that shows the estimated CPU power of the machine running Lighthouse. Example row values: 532, 1492, 783. */
runtimeSettingsBenchmark: 'CPU/Memory Power',
/** Label for a row in a table that shows the version of the Axe library used. Example row values: 2.1.0, 3.2.3 */
runtimeSettingsAxeVersion: 'Axe version',
/** Label for button to create an issue against the Lighthouse Github project. */
footerIssue: 'File an issue',
/** Descriptive explanation for emulation setting when no device emulation is set. */
runtimeNoEmulation: 'No emulation',
/** Descriptive explanation for emulation setting when emulating a Moto G4 mobile device. */
runtimeMobileEmulation: 'Emulated Moto G4',
/** Descriptive explanation for emulation setting when emulating a generic desktop form factor, as opposed to a mobile-device like form factor. */
runtimeDesktopEmulation: 'Emulated Desktop',
/** Descriptive explanation for a runtime setting that is set to an unknown value. */
runtimeUnknown: 'Unknown',
/** Descriptive explanation for environment throttling that was provided by the runtime environment instead of provided by Lighthouse throttling. */
throttlingProvided: 'Provided by environment',
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = Util;
} else {
self.Util = Util;
}
;
/**
* @license
* Copyright 2017 The Lighthouse Authors. All Rights Reserved.
*
* 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
*
* 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.
*/
'use strict';
/* globals self Util */
/** @typedef {HTMLElementTagNameMap & {[id: string]: HTMLElement}} HTMLElementByTagName */
/** @template {string} T @typedef {import('typed-query-selector/parser').ParseSelector<T, Element>} ParseSelector */
class DOM {
/**
* @param {Document} document
*/
constructor(document) {
/** @type {Document} */
this._document = document;
/** @type {string} */
this._lighthouseChannel = 'unknown';
}
/**
* @template {string} T
* @param {T} name
* @param {string=} className
* @param {Object<string, (string|undefined)>=} attrs Attribute key/val pairs.
* Note: if an attribute key has an undefined value, this method does not
* set the attribute on the node.
* @return {HTMLElementByTagName[T]}
*/
createElement(name, className, attrs = {}) {
const element = this._document.createElement(name);
if (className) {
element.className = className;
}
Object.keys(attrs).forEach(key => {
const value = attrs[key];
if (typeof value !== 'undefined') {
element.setAttribute(key, value);
}
});
return element;
}
/**
* @param {string} namespaceURI
* @param {string} name
* @param {string=} className
* @param {Object<string, (string|undefined)>=} attrs Attribute key/val pairs.
* Note: if an attribute key has an undefined value, this method does not
* set the attribute on the node.
* @return {Element}
*/
createElementNS(namespaceURI, name, className, attrs = {}) {
const element = this._document.createElementNS(namespaceURI, name);
if (className) {
element.className = className;
}
Object.keys(attrs).forEach(key => {
const value = attrs[key];
if (typeof value !== 'undefined') {
element.setAttribute(key, value);
}
});
return element;
}
/**
* @return {!DocumentFragment}
*/
createFragment() {
return this._document.createDocumentFragment();
}
/**
* @template {string} T
* @param {Element} parentElem
* @param {T} elementName
* @param {string=} className
* @param {Object<string, (string|undefined)>=} attrs Attribute key/val pairs.
* Note: if an attribute key has an undefined value, this method does not
* set the attribute on the node.
* @return {HTMLElementByTagName[T]}
*/
createChildOf(parentElem, elementName, className, attrs) {
const element = this.createElement(elementName, className, attrs);
parentElem.appendChild(element);
return element;
}
/**
* @param {string} selector
* @param {ParentNode} context
* @return {!DocumentFragment} A clone of the template content.
* @throws {Error}
*/
cloneTemplate(selector, context) {
const template = /** @type {?HTMLTemplateElement} */ (context.querySelector(selector));
if (!template) {
throw new Error(`Template not found: template${selector}`);
}
const clone = this._document.importNode(template.content, true);
// Prevent duplicate styles in the DOM. After a template has been stamped
// for the first time, remove the clone's styles so they're not re-added.
if (template.hasAttribute('data-stamped')) {
this.findAll('style', clone).forEach(style => style.remove());
}
template.setAttribute('data-stamped', 'true');
return clone;
}
/**
* Resets the "stamped" state of the templates.
*/
resetTemplates() {
this.findAll('template[data-stamped]', this._document).forEach(t => {
t.removeAttribute('data-stamped');
});
}
/**
* @param {string} text
* @return {Element}
*/
convertMarkdownLinkSnippets(text) {
const element = this.createElement('span');
for (const segment of Util.splitMarkdownLink(text)) {
if (!segment.isLink) {
// Plain text segment.
element.appendChild(this._document.createTextNode(segment.text));
continue;
}
// Otherwise, append any links found.
const url = new URL(segment.linkHref);
const DOCS_ORIGINS = ['https://developers.google.com', 'https://web.dev'];
if (DOCS_ORIGINS.includes(url.origin)) {
url.searchParams.set('utm_source', 'lighthouse');
url.searchParams.set('utm_medium', this._lighthouseChannel);
}
const a = this.createElement('a');
a.rel = 'noopener';
a.target = '_blank';
a.textContent = segment.text;
a.href = url.href;
element.appendChild(a);
}
return element;
}
/**
* @param {string} markdownText
* @return {Element}
*/
convertMarkdownCodeSnippets(markdownText) {
const element = this.createElement('span');
for (const segment of Util.splitMarkdownCodeSpans(markdownText)) {
if (segment.isCode) {
const pre = this.createElement('code');
pre.textContent = segment.text;
element.appendChild(pre);
} else {
element.appendChild(this._document.createTextNode(segment.text));
}
}
return element;
}
/**
* The channel to use for UTM data when rendering links to the documentation.
* @param {string} lighthouseChannel
*/
setLighthouseChannel(lighthouseChannel) {
this._lighthouseChannel = lighthouseChannel;
}
/**
* @return {Document}
*/
document() {
return this._document;
}
/**
* TODO(paulirish): import and conditionally apply the DevTools frontend subclasses instead of this
* @return {boolean}
*/
isDevTools() {
return !!this._document.querySelector('.lh-devtools');
}
/**
* Guaranteed context.querySelector. Always returns an element or throws if
* nothing matches query.
* @template {string} T
* @param {T} query
* @param {ParentNode} context
* @return {ParseSelector<T>}
*/
find(query, context) {
const result = context.querySelector(query);
if (result === null) {
throw new Error(`query ${query} not found`);
}
// Because we control the report layout and templates, use the simpler
// `typed-query-selector` types that don't require differentiating between
// e.g. HTMLAnchorElement and SVGAElement. See https://github.com/GoogleChrome/lighthouse/issues/12011
return /** @type {ParseSelector<T>} */ (result);
}
/**
* Helper for context.querySelectorAll. Returns an Array instead of a NodeList.
* @template {string} T
* @param {T} query
* @param {ParentNode} context
*/
findAll(query, context) {
const elements = Array.from(context.querySelectorAll(query));
return elements;
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = DOM;
} else {
self.DOM = DOM;
}
;
/**
* @license
* Copyright 2017 The Lighthouse Authors. All Rights Reserved.
*
* 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
*
* 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.
*/
'use strict';
/* globals self CriticalRequestChainRenderer SnippetRenderer ElementScreenshotRenderer Util */
/** @typedef {import('./dom.js')} DOM */
// Convenience types for localized AuditDetails.
/** @typedef {LH.FormattedIcu<LH.Audit.Details>} AuditDetails */
/** @typedef {LH.FormattedIcu<LH.Audit.Details.Opportunity>} OpportunityTable */
/** @typedef {LH.FormattedIcu<LH.Audit.Details.Table>} Table */
/** @typedef {LH.FormattedIcu<LH.Audit.Details.TableItem>} TableItem */
/** @typedef {LH.FormattedIcu<LH.Audit.Details.ItemValue>} TableItemValue */
const URL_PREFIXES = ['http://', 'https://', 'data:'];
class DetailsRenderer {
/**
* @param {DOM} dom
* @param {{fullPageScreenshot?: LH.Artifacts.FullPageScreenshot}} [options]
*/
constructor(dom, options = {}) {
this._dom = dom;
this._fullPageScreenshot = options.fullPageScreenshot;
/** @type {ParentNode} */
this._templateContext; // eslint-disable-line no-unused-expressions
}
/**
* @param {ParentNode} context
*/
setTemplateContext(context) {
this._templateContext = context;
}
/**
* @param {AuditDetails} details
* @return {Element|null}
*/
render(details) {
switch (details.type) {
case 'filmstrip':
return this._renderFilmstrip(details);
case 'list':
return this._renderList(details);
case 'table':
return this._renderTable(details);
case 'criticalrequestchain':
return CriticalRequestChainRenderer.render(this._dom, this._templateContext, details, this);
case 'opportunity':
return this._renderTable(details);
// Internal-only details, not for rendering.
case 'screenshot':
case 'debugdata':
case 'full-page-screenshot':
case 'treemap-data':
return null;
default: {
// @ts-expect-error tsc thinks this is unreachable, but be forward compatible
// with new unexpected detail types.
return this._renderUnknown(details.type, details);
}
}
}
/**
* @param {{value: number, granularity?: number}} details
* @return {Element}
*/
_renderBytes(details) {
// TODO: handle displayUnit once we have something other than 'kb'
// Note that 'kb' is historical and actually represents KiB.
const value = Util.i18n.formatBytesToKiB(details.value, details.granularity);
const textEl = this._renderText(value);
textEl.title = Util.i18n.formatBytes(details.value);
return textEl;
}
/**
* @param {{value: number, granularity?: number, displayUnit?: string}} details
* @return {Element}
*/
_renderMilliseconds(details) {
let value = Util.i18n.formatMilliseconds(details.value, details.granularity);
if (details.displayUnit === 'duration') {
value = Util.i18n.formatDuration(details.value);
}
return this._renderText(value);
}
/**
* @param {string} text
* @return {HTMLElement}
*/
renderTextURL(text) {
const url = text;
let displayedPath;
let displayedHost;
let title;
try {
const parsed = Util.parseURL(url);
displayedPath = parsed.file === '/' ? parsed.origin : parsed.file;
displayedHost = parsed.file === '/' || parsed.hostname === '' ? '' : `(${parsed.hostname})`;
title = url;
} catch (e) {
displayedPath = url;
}
const element = this._dom.createElement('div', 'lh-text__url');
element.appendChild(this._renderLink({text: displayedPath, url}));
if (displayedHost) {
const hostElem = this._renderText(displayedHost);
hostElem.classList.add('lh-text__url-host');
element.appendChild(hostElem);
}
if (title) {
element.title = url;
// set the url on the element's dataset which we use to check 3rd party origins
element.dataset.url = url;
}
return element;
}
/**
* @param {{text: string, url: string}} details
* @return {HTMLElement}
*/
_renderLink(details) {
const allowedProtocols = ['https:', 'http:'];
let url;
try {
url = new URL(details.url);
} catch (_) {}
if (!url || !allowedProtocols.includes(url.protocol)) {
// Fall back to just the link text if invalid or protocol not allowed.
const element = this._renderText(details.text);
element.classList.add('lh-link');
return element;
}
const a = this._dom.createElement('a');
a.rel = 'noopener';
a.target = '_blank';
a.textContent = details.text;
a.href = url.href;
a.classList.add('lh-link');
return a;
}
/**
* @param {string} text
* @return {HTMLDivElement}
*/
_renderText(text) {
const element = this._dom.createElement('div', 'lh-text');
element.textContent = text;
return element;
}
/**
* @param {{value: number, granularity?: number}} details
* @return {Element}
*/
_renderNumeric(details) {
const value = Util.i18n.formatNumber(details.value, details.granularity);
const element = this._dom.createElement('div', 'lh-numeric');
element.textContent = value;
return element;
}
/**
* Create small thumbnail with scaled down image asset.
* @param {string} details
* @return {Element}
*/
_renderThumbnail(details) {
const element = this._dom.createElement('img', 'lh-thumbnail');
const strValue = details;
element.src = strValue;
element.title = strValue;
element.alt = '';
return element;
}
/**
* @param {string} type
* @param {*} value
*/
_renderUnknown(type, value) {
// eslint-disable-next-line no-console
console.error(`Unknown details type: ${type}`, value);
const element = this._dom.createElement('details', 'lh-unknown');
this._dom.createChildOf(element, 'summary').textContent =
`We don't know how to render audit details of type \`${type}\`. ` +
'The Lighthouse version that collected this data is likely newer than the Lighthouse ' +
'version of the report renderer. Expand for the raw JSON.';
this._dom.createChildOf(element, 'pre').textContent = JSON.stringify(value, null, 2);
return element;
}
/**
* Render a details item value for embedding in a table. Renders the value
* based on the heading's valueType, unless the value itself has a `type`
* property to override it.
* @param {TableItemValue} value
* @param {LH.Audit.Details.OpportunityColumnHeading} heading
* @return {Element|null}
*/
_renderTableValue(value, heading) {
if (value === undefined || value === null) {
return null;
}
// First deal with the possible object forms of value.
if (typeof value === 'object') {
// The value's type overrides the heading's for this column.
switch (value.type) {
case 'code': {
return this._renderCode(value.value);
}
case 'link': {
return this._renderLink(value);
}
case 'node': {
return this.renderNode(value);
}
case 'numeric': {
return this._renderNumeric(value);
}
case 'source-location': {
return this.renderSourceLocation(value);
}
case 'url': {
return this.renderTextURL(value.value);
}
default: {
return this._renderUnknown(value.type, value);
}
}
}
// Next, deal with primitives.
switch (heading.valueType) {
case 'bytes': {
const numValue = Number(value);
return this._renderBytes({value: numValue, granularity: heading.granularity});
}
case 'code': {
const strValue = String(value);
return this._renderCode(strValue);
}
case 'ms': {
const msValue = {
value: Number(value),
granularity: heading.granularity,
displayUnit: heading.displayUnit,
};
return this._renderMilliseconds(msValue);
}
case 'numeric': {
const numValue = Number(value);
return this._renderNumeric({value: numValue, granularity: heading.granularity});
}
case 'text': {
const strValue = String(value);
return this._renderText(strValue);
}
case 'thumbnail': {
const strValue = String(value);
return this._renderThumbnail(strValue);
}
case 'timespanMs': {
const numValue = Number(value);
return this._renderMilliseconds({value: numValue});
}
case 'url': {
const strValue = String(value);
if (URL_PREFIXES.some(prefix => strValue.startsWith(prefix))) {
return this.renderTextURL(strValue);
} else {
// Fall back to <pre> rendering if not actually a URL.
return this._renderCode(strValue);
}
}
default: {
return this._renderUnknown(heading.valueType, value);
}
}
}
/**
* Get the headings of a table-like details object, converted into the
* OpportunityColumnHeading type until we have all details use the same
* heading format.
* @param {Table|OpportunityTable} tableLike
* @return {OpportunityTable['headings']}
*/
_getCanonicalizedHeadingsFromTable(tableLike) {
if (tableLike.type === 'opportunity') {
return tableLike.headings;
}
return tableLike.headings.map(heading => this._getCanonicalizedHeading(heading));
}
/**
* Get the headings of a table-like details object, converted into the
* OpportunityColumnHeading type until we have all details use the same
* heading format.
* @param {Table['headings'][number]} heading
* @return {OpportunityTable['headings'][number]}
*/
_getCanonicalizedHeading(heading) {
let subItemsHeading;
if (heading.subItemsHeading) {
subItemsHeading = this._getCanonicalizedsubItemsHeading(heading.subItemsHeading, heading);
}
return {
key: heading.key,
valueType: heading.itemType,
subItemsHeading,
label: heading.text,
displayUnit: heading.displayUnit,
granularity: heading.granularity,
};
}
/**
* @param {Exclude<LH.Audit.Details.TableColumnHeading['subItemsHeading'], undefined>} subItemsHeading
* @param {LH.Audit.Details.TableColumnHeading} parentHeading
* @return {LH.Audit.Details.OpportunityColumnHeading['subItemsHeading']}
*/
_getCanonicalizedsubItemsHeading(subItemsHeading, parentHeading) {
// Low-friction way to prevent commiting a falsy key (which is never allowed for
// a subItemsHeading) from passing in CI.
if (!subItemsHeading.key) {
// eslint-disable-next-line no-console
console.warn('key should not be null');
}
return {
key: subItemsHeading.key || '',
valueType: subItemsHeading.itemType || parentHeading.itemType,
granularity: subItemsHeading.granularity || parentHeading.granularity,
displayUnit: subItemsHeading.displayUnit || parentHeading.displayUnit,
};
}
/**
* Returns a new heading where the values are defined first by `heading.subItemsHeading`,
* and secondly by `heading`. If there is no subItemsHeading, returns null, which will
* be rendered as an empty column.
* @param {LH.Audit.Details.OpportunityColumnHeading} heading
* @return {LH.Audit.Details.OpportunityColumnHeading | null}
*/
_getDerivedsubItemsHeading(heading) {
if (!heading.subItemsHeading) return null;
return {
key: heading.subItemsHeading.key || '',
valueType: heading.subItemsHeading.valueType || heading.valueType,
granularity: heading.subItemsHeading.granularity || heading.granularity,
displayUnit: heading.subItemsHeading.displayUnit || heading.displayUnit,
label: '',
};
}
/**
* @param {TableItem} item
* @param {(LH.Audit.Details.OpportunityColumnHeading | null)[]} headings
*/
_renderTableRow(item, headings) {
const rowElem = this._dom.createElement('tr');
for (const heading of headings) {
// Empty cell if no heading or heading key for this column.
if (!heading || !heading.key) {
this._dom.createChildOf(rowElem, 'td', 'lh-table-column--empty');
continue;
}
const value = item[heading.key];
let valueElement;
if (value !== undefined && value !== null) {
valueElement = this._renderTableValue(value, heading);
}
if (valueElement) {
const classes = `lh-table-column--${heading.valueType}`;
this._dom.createChildOf(rowElem, 'td', classes).appendChild(valueElement);
} else {
// Empty cell is rendered for a column if:
// - the pair is null
// - the heading key is null
// - the value is undefined/null
this._dom.createChildOf(rowElem, 'td', 'lh-table-column--empty');
}
}
return rowElem;
}
/**
* Renders one or more rows from a details table item. A single table item can
* expand into multiple rows, if there is a subItemsHeading.
* @param {TableItem} item
* @param {LH.Audit.Details.OpportunityColumnHeading[]} headings
*/
_renderTableRowsFromItem(item, headings) {
const fragment = this._dom.createFragment();
fragment.append(this._renderTableRow(item, headings));
if (!item.subItems) return fragment;
const subItemsHeadings = headings.map(this._getDerivedsubItemsHeading);
if (!subItemsHeadings.some(Boolean)) return fragment;
for (const subItem of item.subItems.items) {
const rowEl = this._renderTableRow(subItem, subItemsHeadings);
rowEl.classList.add('lh-sub-item-row');
fragment.append(rowEl);
}
return fragment;
}
/**
* @param {OpportunityTable|Table} details
* @return {Element}
*/
_renderTable(details) {
if (!details.items.length) return this._dom.createElement('span');
const tableElem = this._dom.createElement('table', 'lh-table');
const theadElem = this._dom.createChildOf(tableElem, 'thead');
const theadTrElem = this._dom.createChildOf(theadElem, 'tr');
const headings = this._getCanonicalizedHeadingsFromTable(details);
for (const heading of headings) {
const valueType = heading.valueType || 'text';
const classes = `lh-table-column--${valueType}`;
const labelEl = this._dom.createElement('div', 'lh-text');
labelEl.textContent = heading.label;
this._dom.createChildOf(theadTrElem, 'th', classes).appendChild(labelEl);
}
const tbodyElem = this._dom.createChildOf(tableElem, 'tbody');
let even = true;
for (const item of details.items) {
const rowsFragment = this._renderTableRowsFromItem(item, headings);
for (const rowEl of this._dom.findAll('tr', rowsFragment)) {
// For zebra styling.
rowEl.classList.add(even ? 'lh-row--even' : 'lh-row--odd');
}
even = !even;
tbodyElem.append(rowsFragment);
}
return tableElem;
}
/**
* @param {LH.Audit.Details.List} details
* @return {Element}
*/
_renderList(details) {
const listContainer = this._dom.createElement('div', 'lh-list');
details.items.forEach(item => {
const snippetEl = SnippetRenderer.render(this._dom, this._templateContext, item, this);
listContainer.appendChild(snippetEl);
});
return listContainer;
}
/**
* @param {LH.Audit.Details.NodeValue} item
* @return {Element}
*/
renderNode(item) {
const element = this._dom.createElement('span', 'lh-node');
if (item.nodeLabel) {
const nodeLabelEl = this._dom.createElement('div');
nodeLabelEl.textContent = item.nodeLabel;
element.appendChild(nodeLabelEl);
}
if (item.snippet) {
const snippetEl = this._dom.createElement('div');
snippetEl.classList.add('lh-node__snippet');
snippetEl.textContent = item.snippet;
element.appendChild(snippetEl);
}
if (item.selector) {
element.title = item.selector;
}
if (item.path) element.setAttribute('data-path', item.path);
if (item.selector) element.setAttribute('data-selector', item.selector);
if (item.snippet) element.setAttribute('data-snippet', item.snippet);
if (!this._fullPageScreenshot) return element;
const rect = item.lhId && this._fullPageScreenshot.nodes[item.lhId];
if (!rect || rect.width === 0 || rect.height === 0) return element;
const maxThumbnailSize = {width: 147, height: 100};
const elementScreenshot = ElementScreenshotRenderer.render(
this._dom,
this._templateContext,
this._fullPageScreenshot.screenshot,
rect,
maxThumbnailSize
);
if (elementScreenshot) element.prepend(elementScreenshot);
return element;
}
/**
* @param {LH.Audit.Details.SourceLocationValue} item
* @return {Element|null}
* @protected
*/
renderSourceLocation(item) {
if (!item.url) {
return null;
}
// Lines are shown as one-indexed.
const generatedLocation = `${item.url}:${item.line + 1}:${item.column}`;
let sourceMappedOriginalLocation;
if (item.original) {
const file = item.original.file || '<unmapped>';
sourceMappedOriginalLocation = `${file}:${item.original.line + 1}:${item.original.column}`;
}
// We render slightly differently based on presence of source map and provenance of URL.
let element;
if (item.urlProvider === 'network' && sourceMappedOriginalLocation) {
element = this._renderLink({
url: item.url,
text: sourceMappedOriginalLocation,
});
element.title = `maps to generated location ${generatedLocation}`;
} else if (item.urlProvider === 'network' && !sourceMappedOriginalLocation) {
element = this.renderTextURL(item.url);
this._dom.find('.lh-link', element).textContent += `:${item.line + 1}:${item.column}`;
} else if (item.urlProvider === 'comment' && sourceMappedOriginalLocation) {
element = this._renderText(`${sourceMappedOriginalLocation} (from source map)`);
element.title = `${generatedLocation} (from sourceURL)`;
} else if (item.urlProvider === 'comment' && !sourceMappedOriginalLocation) {
element = this._renderText(`${generatedLocation} (from sourceURL)`);
} else {
return null;
}
element.classList.add('lh-source-location');
element.setAttribute('data-source-url', item.url);
// DevTools expects zero-indexed lines.
element.setAttribute('data-source-line', String(item.line));
element.setAttribute('data-source-column', String(item.column));
return element;
}
/**
* @param {LH.Audit.Details.Filmstrip} details
* @return {Element}
*/
_renderFilmstrip(details) {
const filmstripEl = this._dom.createElement('div', 'lh-filmstrip');
for (const thumbnail of details.items) {
const frameEl = this._dom.createChildOf(filmstripEl, 'div', 'lh-filmstrip__frame');
this._dom.createChildOf(frameEl, 'img', 'lh-filmstrip__thumbnail', {
src: thumbnail.data,
alt: `Screenshot`,
});
}
return filmstripEl;
}
/**
* @param {string} text
* @return {Element}
*/
_renderCode(text) {
const pre = this._dom.createElement('pre', 'lh-code');
pre.textContent = text;
return pre;
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = DetailsRenderer;
} else {
self.DetailsRenderer = DetailsRenderer;
}
;
/**
* @license
* Copyright 2017 The Lighthouse Authors. All Rights Reserved.
*
* 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
*
* 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.
*/
'use strict';
/**
* @fileoverview This file contains helpers for constructing and rendering the
* critical request chains network tree.
*/
/* globals self Util */
/** @typedef {import('./dom.js')} DOM */
/** @typedef {import('./details-renderer.js')} DetailsRenderer */
class CriticalRequestChainRenderer {
/**
* Create render context for critical-request-chain tree display.
* @param {LH.Audit.SimpleCriticalRequestNode} tree
* @return {{tree: LH.Audit.SimpleCriticalRequestNode, startTime: number, transferSize: number}}
*/
static initTree(tree) {
let startTime = 0;
const rootNodes = Object.keys(tree);
if (rootNodes.length > 0) {
const node = tree[rootNodes[0]];
startTime = node.request.startTime;
}
return {tree, startTime, transferSize: 0};
}
/**
* Helper to create context for each critical-request-chain node based on its
* parent. Calculates if this node is the last child, whether it has any
* children itself and what the tree looks like all the way back up to the root,
* so the tree markers can be drawn correctly.
* @param {LH.Audit.SimpleCriticalRequestNode} parent
* @param {string} id
* @param {number} startTime
* @param {number} transferSize
* @param {Array<boolean>=} treeMarkers
* @param {boolean=} parentIsLastChild
* @return {CRCSegment}
*/
static createSegment(parent, id, startTime, transferSize, treeMarkers, parentIsLastChild) {
const node = parent[id];
const siblings = Object.keys(parent);
const isLastChild = siblings.indexOf(id) === (siblings.length - 1);
const hasChildren = !!node.children && Object.keys(node.children).length > 0;
// Copy the tree markers so that we don't change by reference.
const newTreeMarkers = Array.isArray(treeMarkers) ? treeMarkers.slice(0) : [];
// Add on the new entry.
if (typeof parentIsLastChild !== 'undefined') {
newTreeMarkers.push(!parentIsLastChild);
}
return {
node,
isLastChild,
hasChildren,
startTime,
transferSize: transferSize + node.request.transferSize,
treeMarkers: newTreeMarkers,
};
}
/**
* Creates the DOM for a tree segment.
* @param {DOM} dom
* @param {DocumentFragment} tmpl
* @param {CRCSegment} segment
* @param {DetailsRenderer} detailsRenderer
* @return {Node}
*/
static createChainNode(dom, tmpl, segment, detailsRenderer) {
const chainsEl = dom.cloneTemplate('#tmpl-lh-crc__chains', tmpl);
// Hovering over request shows full URL.
dom.find('.crc-node', chainsEl).setAttribute('title', segment.node.request.url);
const treeMarkeEl = dom.find('.crc-node__tree-marker', chainsEl);
// Construct lines and add spacers for sub requests.
segment.treeMarkers.forEach(separator => {
if (separator) {
treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker vert'));
treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker'));
} else {
treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker'));
treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker'));
}
});
if (segment.isLastChild) {
treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker up-right'));
treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker right'));
} else {
treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker vert-right'));
treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker right'));
}
if (segment.hasChildren) {
treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker horiz-down'));
} else {
treeMarkeEl.appendChild(dom.createElement('span', 'tree-marker right'));
}
// Fill in url, host, and request size information.
const url = segment.node.request.url;
const linkEl = detailsRenderer.renderTextURL(url);
const treevalEl = dom.find('.crc-node__tree-value', chainsEl);
treevalEl.appendChild(linkEl);
if (!segment.hasChildren) {
const {startTime, endTime, transferSize} = segment.node.request;
const span = dom.createElement('span', 'crc-node__chain-duration');
span.textContent = ' - ' + Util.i18n.formatMilliseconds((endTime - startTime) * 1000) + ', ';
const span2 = dom.createElement('span', 'crc-node__chain-duration');
span2.textContent = Util.i18n.formatBytesToKiB(transferSize, 0.01);
treevalEl.appendChild(span);
treevalEl.appendChild(span2);
}
return chainsEl;
}
/**
* Recursively builds a tree from segments.
* @param {DOM} dom
* @param {DocumentFragment} tmpl
* @param {CRCSegment} segment
* @param {Element} elem Parent element.
* @param {LH.Audit.Details.CriticalRequestChain} details
* @param {DetailsRenderer} detailsRenderer
*/
static buildTree(dom, tmpl, segment, elem, details, detailsRenderer) {
elem.appendChild(CRCRenderer.createChainNode(dom, tmpl, segment, detailsRenderer));
if (segment.node.children) {
for (const key of Object.keys(segment.node.children)) {
const childSegment = CRCRenderer.createSegment(segment.node.children, key,
segment.startTime, segment.transferSize, segment.treeMarkers, segment.isLastChild);
CRCRenderer.buildTree(dom, tmpl, childSegment, elem, details, detailsRenderer);
}
}
}
/**
* @param {DOM} dom
* @param {ParentNode} templateContext
* @param {LH.Audit.Details.CriticalRequestChain} details
* @param {DetailsRenderer} detailsRenderer
* @return {Element}
*/
static render(dom, templateContext, details, detailsRenderer) {
const tmpl = dom.cloneTemplate('#tmpl-lh-crc', templateContext);
const containerEl = dom.find('.lh-crc', tmpl);
// Fill in top summary.
dom.find('.crc-initial-nav', tmpl).textContent = Util.i18n.strings.crcInitialNavigation;
dom.find('.lh-crc__longest_duration_label', tmpl).textContent =
Util.i18n.strings.crcLongestDurationLabel;
dom.find('.lh-crc__longest_duration', tmpl).textContent =
Util.i18n.formatMilliseconds(details.longestChain.duration);
// Construct visual tree.
const root = CRCRenderer.initTree(details.chains);
for (const key of Object.keys(root.tree)) {
const segment = CRCRenderer.createSegment(root.tree, key, root.startTime, root.transferSize);
CRCRenderer.buildTree(dom, tmpl, segment, containerEl, details, detailsRenderer);
}
return dom.find('.lh-crc-container', tmpl);
}
}
// Alias b/c the name is really long.
const CRCRenderer = CriticalRequestChainRenderer;
// Allow Node require()'ing.
if (typeof module !== 'undefined' && module.exports) {
module.exports = CriticalRequestChainRenderer;
} else {
self.CriticalRequestChainRenderer = CriticalRequestChainRenderer;
}
/** @typedef {{
node: LH.Audit.SimpleCriticalRequestNode[string],
isLastChild: boolean,
hasChildren: boolean,
startTime: number,
transferSize: number,
treeMarkers: Array<boolean>
}} CRCSegment
*/
;
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* 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
* 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.
*/
'use strict';
/* globals self, Util */
/** @typedef {import('./details-renderer')} DetailsRenderer */
/** @typedef {import('./dom')} DOM */
/** @enum {number} */
const LineVisibility = {
/** Show regardless of whether the snippet is collapsed or expanded */
ALWAYS: 0,
WHEN_COLLAPSED: 1,
WHEN_EXPANDED: 2,
};
/** @enum {number} */
const LineContentType = {
/** A line of content */
CONTENT_NORMAL: 0,
/** A line of content that's emphasized by setting the CSS background color */
CONTENT_HIGHLIGHTED: 1,
/** Use when some lines are hidden, shows the "..." placeholder */
PLACEHOLDER: 2,
/** A message about a line of content or the snippet in general */
MESSAGE: 3,
};
/** @typedef {{
content: string;
lineNumber: string | number;
contentType: LineContentType;
truncated?: boolean;
visibility?: LineVisibility;
}} LineDetails */
const classNamesByContentType = {
[LineContentType.CONTENT_NORMAL]: ['lh-snippet__line--content'],
[LineContentType.CONTENT_HIGHLIGHTED]: [
'lh-snippet__line--content',
'lh-snippet__line--content-highlighted',
],
[LineContentType.PLACEHOLDER]: ['lh-snippet__line--placeholder'],
[LineContentType.MESSAGE]: ['lh-snippet__line--message'],
};
/**
* @param {LH.Audit.Details.SnippetValue['lines']} lines
* @param {number} lineNumber
* @return {{line?: LH.Audit.Details.SnippetValue['lines'][0], previousLine?: LH.Audit.Details.SnippetValue['lines'][0]}}
*/
function getLineAndPreviousLine(lines, lineNumber) {
return {
line: lines.find(l => l.lineNumber === lineNumber),
previousLine: lines.find(l => l.lineNumber === lineNumber - 1),
};
}
/**
* @param {LH.Audit.Details.SnippetValue["lineMessages"]} messages
* @param {number} lineNumber
*/
function getMessagesForLineNumber(messages, lineNumber) {
return messages.filter(h => h.lineNumber === lineNumber);
}
/**
* @param {LH.Audit.Details.SnippetValue} details
* @return {LH.Audit.Details.SnippetValue['lines']}
*/
function getLinesWhenCollapsed(details) {
const SURROUNDING_LINES_TO_SHOW_WHEN_COLLAPSED = 2;
return Util.filterRelevantLines(
details.lines,
details.lineMessages,
SURROUNDING_LINES_TO_SHOW_WHEN_COLLAPSED
);
}
/**
* Render snippet of text with line numbers and annotations.
* By default we only show a few lines around each annotation and the user
* can click "Expand snippet" to show more.
* Content lines with annotations are highlighted.
*/
class SnippetRenderer {
/**
* @param {DOM} dom
* @param {DocumentFragment} tmpl
* @param {LH.Audit.Details.SnippetValue} details
* @param {DetailsRenderer} detailsRenderer
* @param {function} toggleExpandedFn
* @return {DocumentFragment}
*/
static renderHeader(dom, tmpl, details, detailsRenderer, toggleExpandedFn) {
const linesWhenCollapsed = getLinesWhenCollapsed(details);
const canExpand = linesWhenCollapsed.length < details.lines.length;
const header = dom.cloneTemplate('#tmpl-lh-snippet__header', tmpl);
dom.find('.lh-snippet__title', header).textContent = details.title;
const {
snippetCollapseButtonLabel,
snippetExpandButtonLabel,
} = Util.i18n.strings;
dom.find(
'.lh-snippet__btn-label-collapse',
header
).textContent = snippetCollapseButtonLabel;
dom.find(
'.lh-snippet__btn-label-expand',
header
).textContent = snippetExpandButtonLabel;
const toggleExpandButton = dom.find('.lh-snippet__toggle-expand', header);
// If we're already showing all the available lines of the snippet, we don't need an
// expand/collapse button and can remove it from the DOM.
// If we leave the button in though, wire up the click listener to toggle visibility!
if (!canExpand) {
toggleExpandButton.remove();
} else {
toggleExpandButton.addEventListener('click', () => toggleExpandedFn());
}
// We only show the source node of the snippet in DevTools because then the user can
// access the full element detail. Just being able to see the outer HTML isn't very useful.
if (details.node && dom.isDevTools()) {
const nodeContainer = dom.find('.lh-snippet__node', header);
nodeContainer.appendChild(detailsRenderer.renderNode(details.node));
}
return header;
}
/**
* Renders a line (text content, message, or placeholder) as a DOM element.
* @param {DOM} dom
* @param {DocumentFragment} tmpl
* @param {LineDetails} lineDetails
* @return {Element}
*/
static renderSnippetLine(
dom,
tmpl,
{content, lineNumber, truncated, contentType, visibility}
) {
const clonedTemplate = dom.cloneTemplate('#tmpl-lh-snippet__line', tmpl);
const contentLine = dom.find('.lh-snippet__line', clonedTemplate);
const {classList} = contentLine;
classNamesByContentType[contentType].forEach(typeClass =>
classList.add(typeClass)
);
if (visibility === LineVisibility.WHEN_COLLAPSED) {
classList.add('lh-snippet__show-if-collapsed');
} else if (visibility === LineVisibility.WHEN_EXPANDED) {
classList.add('lh-snippet__show-if-expanded');
}
const lineContent = content + (truncated ? '…' : '');
const lineContentEl = dom.find('.lh-snippet__line code', contentLine);
if (contentType === LineContentType.MESSAGE) {
lineContentEl.appendChild(dom.convertMarkdownLinkSnippets(lineContent));
} else {
lineContentEl.textContent = lineContent;
}
dom.find(
'.lh-snippet__line-number',
contentLine
).textContent = lineNumber.toString();
return contentLine;
}
/**
* @param {DOM} dom
* @param {DocumentFragment} tmpl
* @param {{message: string}} message
* @return {Element}
*/
static renderMessage(dom, tmpl, message) {
return SnippetRenderer.renderSnippetLine(dom, tmpl, {
lineNumber: ' ',
content: message.message,
contentType: LineContentType.MESSAGE,
});
}
/**
* @param {DOM} dom
* @param {DocumentFragment} tmpl
* @param {LineVisibility} visibility
* @return {Element}
*/
static renderOmittedLinesPlaceholder(dom, tmpl, visibility) {
return SnippetRenderer.renderSnippetLine(dom, tmpl, {
lineNumber: '…',
content: '',
visibility,
contentType: LineContentType.PLACEHOLDER,
});
}
/**
* @param {DOM} dom
* @param {DocumentFragment} tmpl
* @param {LH.Audit.Details.SnippetValue} details
* @return {DocumentFragment}
*/
static renderSnippetContent(dom, tmpl, details) {
const template = dom.cloneTemplate('#tmpl-lh-snippet__content', tmpl);
const snippetEl = dom.find('.lh-snippet__snippet-inner', template);
// First render messages that don't belong to specific lines
details.generalMessages.forEach(m =>
snippetEl.append(SnippetRenderer.renderMessage(dom, tmpl, m))
);
// Then render the lines and their messages, as well as placeholders where lines are omitted
snippetEl.append(SnippetRenderer.renderSnippetLines(dom, tmpl, details));
return template;
}
/**
* @param {DOM} dom
* @param {DocumentFragment} tmpl
* @param {LH.Audit.Details.SnippetValue} details
* @return {DocumentFragment}
*/
static renderSnippetLines(dom, tmpl, details) {
const {lineMessages, generalMessages, lineCount, lines} = details;
const linesWhenCollapsed = getLinesWhenCollapsed(details);
const hasOnlyGeneralMessages =
generalMessages.length > 0 && lineMessages.length === 0;
const lineContainer = dom.createFragment();
// When a line is not shown in the collapsed state we try to see if we also need an
// omitted lines placeholder for the expanded state, rather than rendering two separate
// placeholders.
let hasPendingOmittedLinesPlaceholderForCollapsedState = false;
for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) {
const {line, previousLine} = getLineAndPreviousLine(lines, lineNumber);
const {
line: lineWhenCollapsed,
previousLine: previousLineWhenCollapsed,
} = getLineAndPreviousLine(linesWhenCollapsed, lineNumber);
const showLineWhenCollapsed = !!lineWhenCollapsed;
const showPreviousLineWhenCollapsed = !!previousLineWhenCollapsed;
// If we went from showing lines in the collapsed state to not showing them
// we need to render a placeholder
if (showPreviousLineWhenCollapsed && !showLineWhenCollapsed) {
hasPendingOmittedLinesPlaceholderForCollapsedState = true;
}
// If we are back to lines being visible in the collapsed and the placeholder
// hasn't been rendered yet then render it now
if (
showLineWhenCollapsed &&
hasPendingOmittedLinesPlaceholderForCollapsedState
) {
lineContainer.append(
SnippetRenderer.renderOmittedLinesPlaceholder(
dom,
tmpl,
LineVisibility.WHEN_COLLAPSED
)
);
hasPendingOmittedLinesPlaceholderForCollapsedState = false;
}
// Render omitted lines placeholder if we have not already rendered one for this gap
const isFirstOmittedLineWhenExpanded = !line && !!previousLine;
const isFirstLineOverallAndIsOmittedWhenExpanded =
!line && lineNumber === 1;
if (
isFirstOmittedLineWhenExpanded ||
isFirstLineOverallAndIsOmittedWhenExpanded
) {
// In the collapsed state we don't show omitted lines placeholders around
// the edges of the snippet
const hasRenderedAllLinesVisibleWhenCollapsed = !linesWhenCollapsed.some(
l => l.lineNumber > lineNumber
);
const onlyShowWhenExpanded =
hasRenderedAllLinesVisibleWhenCollapsed || lineNumber === 1;
lineContainer.append(
SnippetRenderer.renderOmittedLinesPlaceholder(
dom,
tmpl,
onlyShowWhenExpanded
? LineVisibility.WHEN_EXPANDED
: LineVisibility.ALWAYS
)
);
hasPendingOmittedLinesPlaceholderForCollapsedState = false;
}
if (!line) {
// Can't render the line if we don't know its content (instead we've rendered a placeholder)
continue;
}
// Now render the line and any messages
const messages = getMessagesForLineNumber(lineMessages, lineNumber);
const highlightLine = messages.length > 0 || hasOnlyGeneralMessages;
const contentLineDetails = Object.assign({}, line, {
contentType: highlightLine
? LineContentType.CONTENT_HIGHLIGHTED
: LineContentType.CONTENT_NORMAL,
visibility: lineWhenCollapsed
? LineVisibility.ALWAYS
: LineVisibility.WHEN_EXPANDED,
});
lineContainer.append(
SnippetRenderer.renderSnippetLine(dom, tmpl, contentLineDetails)
);
messages.forEach(message => {
lineContainer.append(SnippetRenderer.renderMessage(dom, tmpl, message));
});
}
return lineContainer;
}
/**
* @param {DOM} dom
* @param {ParentNode} templateContext
* @param {LH.Audit.Details.SnippetValue} details
* @param {DetailsRenderer} detailsRenderer
* @return {!Element}
*/
static render(dom, templateContext, details, detailsRenderer) {
const tmpl = dom.cloneTemplate('#tmpl-lh-snippet', templateContext);
const snippetEl = dom.find('.lh-snippet', tmpl);
const header = SnippetRenderer.renderHeader(
dom,
tmpl,
details,
detailsRenderer,
() => snippetEl.classList.toggle('lh-snippet--expanded')
);
const content = SnippetRenderer.renderSnippetContent(dom, tmpl, details);
snippetEl.append(header, content);
return snippetEl;
}
}
// Allow Node require()'ing.
if (typeof module !== 'undefined' && module.exports) {
module.exports = SnippetRenderer;
} else {
self.SnippetRenderer = SnippetRenderer;
}
;
/**
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
* 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
* 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.
*/
'use strict';
/**
* @fileoverview These functions define {Rect}s and {Size}s using two different coordinate spaces:
* 1. Screenshot coords (SC suffix): where 0,0 is the top left of the screenshot image
* 2. Display coords (DC suffix): that match the CSS pixel coordinate space of the LH report's page.
*/
/* globals self Util */
/** @typedef {import('./dom.js')} DOM */
/** @typedef {LH.Artifacts.Rect} Rect */
/** @typedef {{width: number, height: number}} Size */
/**
* @typedef InstallOverlayFeatureParams
* @property {DOM} dom
* @property {Element} reportEl
* @property {Element} overlayContainerEl
* @property {ParentNode} templateContext
* @property {LH.Artifacts.FullPageScreenshot} fullPageScreenshot
*/
/**
* @param {LH.Artifacts.FullPageScreenshot['screenshot']} screenshot
* @param {LH.Artifacts.Rect} rect
* @return {boolean}
*/
function screenshotOverlapsRect(screenshot, rect) {
return rect.left <= screenshot.width &&
0 <= rect.right &&
rect.top <= screenshot.height &&
0 <= rect.bottom;
}
/**
* @param {number} value
* @param {number} min
* @param {number} max
*/
function clamp(value, min, max) {
if (value < min) return min;
if (value > max) return max;
return value;
}
/**
* @param {Rect} rect
*/
function getRectCenterPoint(rect) {
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
};
}
class ElementScreenshotRenderer {
/**
* Given the location of an element and the sizes of the preview and screenshot,
* compute the absolute positions (in screenshot coordinate scale) of the screenshot content
* and the highlighted rect around the element.
* @param {Rect} elementRectSC
* @param {Size} elementPreviewSizeSC
* @param {Size} screenshotSize
*/
static getScreenshotPositions(elementRectSC, elementPreviewSizeSC, screenshotSize) {
const elementRectCenter = getRectCenterPoint(elementRectSC);
// Try to center clipped region.
const screenshotLeftVisibleEdge = clamp(
elementRectCenter.x - elementPreviewSizeSC.width / 2,
0, screenshotSize.width - elementPreviewSizeSC.width
);
const screenshotTopVisisbleEdge = clamp(
elementRectCenter.y - elementPreviewSizeSC.height / 2,
0, screenshotSize.height - elementPreviewSizeSC.height
);
return {
screenshot: {
left: screenshotLeftVisibleEdge,
top: screenshotTopVisisbleEdge,
},
clip: {
left: elementRectSC.left - screenshotLeftVisibleEdge,
top: elementRectSC.top - screenshotTopVisisbleEdge,
},
};
}
/**
* Render a clipPath SVG element to assist marking the element's rect.
* The elementRect and previewSize are in screenshot coordinate scale.
* @param {DOM} dom
* @param {HTMLElement} maskEl
* @param {{left: number, top: number}} positionClip
* @param {LH.Artifacts.Rect} elementRect
* @param {Size} elementPreviewSize
*/
static renderClipPathInScreenshot(dom, maskEl, positionClip, elementRect, elementPreviewSize) {
const clipPathEl = dom.find('clipPath', maskEl);
const clipId = `clip-${Util.getUniqueSuffix()}`;
clipPathEl.id = clipId;
maskEl.style.clipPath = `url(#${clipId})`;
// Normalize values between 0-1.
const top = positionClip.top / elementPreviewSize.height;
const bottom = top + elementRect.height / elementPreviewSize.height;
const left = positionClip.left / elementPreviewSize.width;
const right = left + elementRect.width / elementPreviewSize.width;
const polygonsPoints = [
`0,0 1,0 1,${top} 0,${top}`,
`0,${bottom} 1,${bottom} 1,1 0,1`,
`0,${top} ${left},${top} ${left},${bottom} 0,${bottom}`,
`${right},${top} 1,${top} 1,${bottom} ${right},${bottom}`,
];
for (const points of polygonsPoints) {
clipPathEl.append(dom.createElementNS(
'http://www.w3.org/2000/svg', 'polygon', undefined, {points}));
}
}
/**
* Called by report renderer. Defines a css variable used by any element screenshots
* in the provided report element.
* Allows for multiple Lighthouse reports to be rendered on the page, each with their
* own full page screenshot.
* @param {HTMLElement} el
* @param {LH.Artifacts.FullPageScreenshot['screenshot']} screenshot
*/
static installFullPageScreenshot(el, screenshot) {
el.style.setProperty('--element-screenshot-url', `url(${screenshot.data})`);
}
/**
* Installs the lightbox elements and wires up click listeners to all .lh-element-screenshot elements.
* @param {InstallOverlayFeatureParams} opts
*/
static installOverlayFeature(opts) {
const {dom, reportEl, overlayContainerEl, templateContext, fullPageScreenshot} = opts;
const screenshotOverlayClass = 'lh-screenshot-overlay--enabled';
// Don't install the feature more than once.
if (reportEl.classList.contains(screenshotOverlayClass)) return;
reportEl.classList.add(screenshotOverlayClass);
// Add a single listener to the provided element to handle all clicks within (event delegation).
reportEl.addEventListener('click', e => {
const target = /** @type {?HTMLElement} */ (e.target);
if (!target) return;
// Only activate the overlay for clicks on the screenshot *preview* of an element, not the full-size too.
const el = /** @type {?HTMLElement} */ (target.closest('.lh-node > .lh-element-screenshot'));
if (!el) return;
const overlay = dom.createElement('div', 'lh-element-screenshot__overlay');
overlayContainerEl.append(overlay);
// The newly-added overlay has the dimensions we need.
const maxLightboxSize = {
width: overlay.clientWidth * 0.95,
height: overlay.clientHeight * 0.80,
};
const elementRectSC = {
width: Number(el.dataset['rectWidth']),
height: Number(el.dataset['rectHeight']),
left: Number(el.dataset['rectLeft']),
right: Number(el.dataset['rectLeft']) + Number(el.dataset['rectWidth']),
top: Number(el.dataset['rectTop']),
bottom: Number(el.dataset['rectTop']) + Number(el.dataset['rectHeight']),
};
const screenshotElement = ElementScreenshotRenderer.render(
dom,
templateContext,
fullPageScreenshot.screenshot,
elementRectSC,
maxLightboxSize
);
// This would be unexpected here.
// When `screenshotElement` is `null`, there is also no thumbnail element for the user to have clicked to make it this far.
if (!screenshotElement) {
overlay.remove();
return;
}
overlay.appendChild(screenshotElement);
overlay.addEventListener('click', () => overlay.remove());
});
}
/**
* Given the size of the element in the screenshot and the total available size of our preview container,
* compute the factor by which we need to zoom out to view the entire element with context.
* @param {LH.Artifacts.Rect} elementRectSC
* @param {Size} renderContainerSizeDC
* @return {number}
*/
static _computeZoomFactor(elementRectSC, renderContainerSizeDC) {
const targetClipToViewportRatio = 0.75;
const zoomRatioXY = {
x: renderContainerSizeDC.width / elementRectSC.width,
y: renderContainerSizeDC.height / elementRectSC.height,
};
const zoomFactor = targetClipToViewportRatio * Math.min(zoomRatioXY.x, zoomRatioXY.y);
return Math.min(1, zoomFactor);
}
/**
* Renders an element with surrounding context from the full page screenshot.
* Used to render both the thumbnail preview in details tables and the full-page screenshot in the lightbox.
* Returns null if element rect is outside screenshot bounds.
* @param {DOM} dom
* @param {ParentNode} templateContext
* @param {LH.Artifacts.FullPageScreenshot['screenshot']} screenshot
* @param {LH.Artifacts.Rect} elementRectSC Region of screenshot to highlight.
* @param {Size} maxRenderSizeDC e.g. maxThumbnailSize or maxLightboxSize.
* @return {Element|null}
*/
static render(dom, templateContext, screenshot, elementRectSC, maxRenderSizeDC) {
if (!screenshotOverlapsRect(screenshot, elementRectSC)) {
return null;
}
const tmpl = dom.cloneTemplate('#tmpl-lh-element-screenshot', templateContext);
const containerEl = dom.find('div.lh-element-screenshot', tmpl);
containerEl.dataset['rectWidth'] = elementRectSC.width.toString();
containerEl.dataset['rectHeight'] = elementRectSC.height.toString();
containerEl.dataset['rectLeft'] = elementRectSC.left.toString();
containerEl.dataset['rectTop'] = elementRectSC.top.toString();
// Zoom out when highlighted region takes up most of the viewport.
// This provides more context for where on the page this element is.
const zoomFactor = this._computeZoomFactor(elementRectSC, maxRenderSizeDC);
const elementPreviewSizeSC = {
width: maxRenderSizeDC.width / zoomFactor,
height: maxRenderSizeDC.height / zoomFactor,
};
elementPreviewSizeSC.width = Math.min(screenshot.width, elementPreviewSizeSC.width);
/* This preview size is either the size of the thumbnail or size of the Lightbox */
const elementPreviewSizeDC = {
width: elementPreviewSizeSC.width * zoomFactor,
height: elementPreviewSizeSC.height * zoomFactor,
};
const positions = ElementScreenshotRenderer.getScreenshotPositions(
elementRectSC,
elementPreviewSizeSC,
{width: screenshot.width, height: screenshot.height}
);
const contentEl = dom.find('div.lh-element-screenshot__content', containerEl);
contentEl.style.top = `-${elementPreviewSizeDC.height}px`;
const imageEl = dom.find('div.lh-element-screenshot__image', containerEl);
imageEl.style.width = elementPreviewSizeDC.width + 'px';
imageEl.style.height = elementPreviewSizeDC.height + 'px';
imageEl.style.backgroundPositionY = -(positions.screenshot.top * zoomFactor) + 'px';
imageEl.style.backgroundPositionX = -(positions.screenshot.left * zoomFactor) + 'px';
imageEl.style.backgroundSize =
`${screenshot.width * zoomFactor}px ${screenshot.height * zoomFactor}px`;
const markerEl = dom.find('div.lh-element-screenshot__element-marker', containerEl);
markerEl.style.width = elementRectSC.width * zoomFactor + 'px';
markerEl.style.height = elementRectSC.height * zoomFactor + 'px';
markerEl.style.left = positions.clip.left * zoomFactor + 'px';
markerEl.style.top = positions.clip.top * zoomFactor + 'px';
const maskEl = dom.find('div.lh-element-screenshot__mask', containerEl);
maskEl.style.width = elementPreviewSizeDC.width + 'px';
maskEl.style.height = elementPreviewSizeDC.height + 'px';
ElementScreenshotRenderer.renderClipPathInScreenshot(
dom,
maskEl,
positions.clip,
elementRectSC,
elementPreviewSizeSC
);
return containerEl;
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = ElementScreenshotRenderer;
} else {
self.ElementScreenshotRenderer = ElementScreenshotRenderer;
}
;
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* 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
* 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.
*/
'use strict';
/**
* @fileoverview
* @suppress {reportUnknownTypes}
*/
/**
* Generate a filenamePrefix of hostname_YYYY-MM-DD_HH-MM-SS
* Date/time uses the local timezone, however Node has unreliable ICU
* support, so we must construct a YYYY-MM-DD date format manually. :/
* @param {{finalUrl: string, fetchTime: string}} lhr
* @return {string}
*/
function getFilenamePrefix(lhr) {
const hostname = new URL(lhr.finalUrl).hostname;
const date = (lhr.fetchTime && new Date(lhr.fetchTime)) || new Date();
const timeStr = date.toLocaleTimeString('en-US', {hour12: false});
const dateParts = date.toLocaleDateString('en-US', {
year: 'numeric', month: '2-digit', day: '2-digit',
}).split('/');
// @ts-expect-error - parts exists
dateParts.unshift(dateParts.pop());
const dateStr = dateParts.join('-');
const filenamePrefix = `${hostname}_${dateStr}_${timeStr}`;
// replace characters that are unfriendly to filenames
return filenamePrefix.replace(/[/?<>\\:*|"]/g, '-');
}
// don't attempt to export in the browser.
if (typeof module !== 'undefined' && module.exports) {
module.exports = {getFilenamePrefix};
}
;
/**
* @license
* Copyright 2017 The Lighthouse Authors. All Rights Reserved.
*
* 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
*
* 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.
*/
'use strict';
/**
* Logs messages via a UI butter.
*/
class Logger {
/**
* @param {Element} element
*/
constructor(element) {
this.el = element;
this._id = undefined;
}
/**
* Shows a butter bar.
* @param {string} msg The message to show.
* @param {boolean=} autoHide True to hide the message after a duration.
* Default is true.
*/
log(msg, autoHide = true) {
this._id && clearTimeout(this._id);
this.el.textContent = msg;
this.el.classList.add('show');
if (autoHide) {
this._id = setTimeout(_ => {
this.el.classList.remove('show');
}, 7000);
}
}
/**
* @param {string} msg
*/
warn(msg) {
this.log('Warning: ' + msg);
}
/**
* @param {string} msg
*/
error(msg) {
this.log(msg);
// Rethrow to make sure it's auditable as an error, but in a setTimeout so page
// recovers gracefully and user can try loading a report again.
setTimeout(_ => {
throw new Error(msg);
}, 0);
}
/**
* Explicitly hides the butter bar.
*/
hide() {
this._id && clearTimeout(this._id);
this.el.classList.remove('show');
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = Logger;
}
;
/**
* @license
* Copyright 2017 The Lighthouse Authors. All Rights Reserved.
*
* 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
*
* 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.
*/
'use strict';
/* eslint-env browser */
/**
* @fileoverview Adds tools button, print, and other dynamic functionality to
* the report.
*/
/* globals getFilenamePrefix Util TextEncoding ElementScreenshotRenderer */
/** @typedef {import('./dom')} DOM */
/**
* @param {HTMLTableElement} tableEl
* @return {Array<HTMLElement>}
*/
function getTableRows(tableEl) {
return Array.from(tableEl.tBodies[0].rows);
}
function getAppsOrigin() {
const isVercel = window.location.host.endsWith('.vercel.app');
const isDev = new URLSearchParams(window.location.search).has('dev');
if (isVercel) return `https://${window.location.host}/gh-pages`;
if (isDev) return 'http://localhost:8000';
return 'https://googlechrome.github.io/lighthouse';
}
class ReportUIFeatures {
/**
* @param {DOM} dom
*/
constructor(dom) {
/** @type {LH.Result} */
this.json; // eslint-disable-line no-unused-expressions
/** @type {DOM} */
this._dom = dom;
/** @type {Document} */
this._document = this._dom.document();
/** @type {ParentNode} */
this._templateContext = this._dom.document();
/** @type {DropDown} */
this._dropDown = new DropDown(this._dom);
/** @type {boolean} */
this._copyAttempt = false;
/** @type {HTMLElement} */
this.topbarEl; // eslint-disable-line no-unused-expressions
/** @type {HTMLElement} */
this.scoreScaleEl; // eslint-disable-line no-unused-expressions
/** @type {HTMLElement} */
this.stickyHeaderEl; // eslint-disable-line no-unused-expressions
/** @type {HTMLElement} */
this.highlightEl; // eslint-disable-line no-unused-expressions
this.onMediaQueryChange = this.onMediaQueryChange.bind(this);
this.onCopy = this.onCopy.bind(this);
this.onDropDownMenuClick = this.onDropDownMenuClick.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
this.collapseAllDetails = this.collapseAllDetails.bind(this);
this.expandAllDetails = this.expandAllDetails.bind(this);
this._toggleDarkTheme = this._toggleDarkTheme.bind(this);
this._updateStickyHeaderOnScroll = this._updateStickyHeaderOnScroll.bind(this);
}
/**
* Adds tools button, print, and other functionality to the report. The method
* should be called whenever the report needs to be re-rendered.
* @param {LH.Result} report
*/
initFeatures(report) {
this.json = report;
this._setupMediaQueryListeners();
this._dropDown.setup(this.onDropDownMenuClick);
this._setupThirdPartyFilter();
this._setupElementScreenshotOverlay(this._dom.find('.lh-container', this._document));
this._setUpCollapseDetailsAfterPrinting();
this._resetUIState();
this._document.addEventListener('keyup', this.onKeyUp);
this._document.addEventListener('copy', this.onCopy);
const topbarLogo = this._dom.find('.lh-topbar__logo', this._document);
topbarLogo.addEventListener('click', () => this._toggleDarkTheme());
let turnOffTheLights = false;
// Do not query the system preferences for DevTools - DevTools should only apply dark theme
// if dark is selected in the settings panel.
if (!this._dom.isDevTools() && window.matchMedia('(prefers-color-scheme: dark)').matches) {
turnOffTheLights = true;
}
// Fireworks!
// To get fireworks you need 100 scores in all core categories, except PWA (because going the PWA route is discretionary).
const fireworksRequiredCategoryIds = ['performance', 'accessibility', 'best-practices', 'seo'];
const scoresAll100 = fireworksRequiredCategoryIds.every(id => {
const cat = report.categories[id];
return cat && cat.score === 1;
});
if (scoresAll100) {
turnOffTheLights = true;
this._enableFireworks();
}
if (turnOffTheLights) {
this._toggleDarkTheme(true);
}
// There is only a sticky header when at least 2 categories are present.
if (Object.keys(this.json.categories).length >= 2) {
this._setupStickyHeaderElements();
const containerEl = this._dom.find('.lh-container', this._document);
const elToAddScrollListener = this._getScrollParent(containerEl);
elToAddScrollListener.addEventListener('scroll', this._updateStickyHeaderOnScroll);
// Use ResizeObserver where available.
// TODO: there is an issue with incorrect position numbers and, as a result, performance
// issues due to layout thrashing.
// See https://github.com/GoogleChrome/lighthouse/pull/9023/files#r288822287 for details.
// For now, limit to DevTools.
if (this._dom.isDevTools()) {
const resizeObserver = new window.ResizeObserver(this._updateStickyHeaderOnScroll);
resizeObserver.observe(containerEl);
} else {
window.addEventListener('resize', this._updateStickyHeaderOnScroll);
}
}
// Show the metric descriptions by default when there is an error.
const hasMetricError = report.categories.performance && report.categories.performance.auditRefs
.some(audit => Boolean(audit.group === 'metrics' && report.audits[audit.id].errorMessage));
if (hasMetricError) {
const toggleInputEl = this._dom.find('input.lh-metrics-toggle__input', this._document);
toggleInputEl.checked = true;
}
const showTreemapApp =
this.json.audits['script-treemap-data'] && this.json.audits['script-treemap-data'].details;
if (showTreemapApp) {
this.addButton({
text: Util.i18n.strings.viewTreemapLabel,
icon: 'treemap',
onClick: () => ReportUIFeatures.openTreemap(this.json),
});
}
// Fill in all i18n data.
for (const node of this._dom.findAll('[data-i18n]', this._dom.document())) {
// These strings are guaranteed to (at least) have a default English string in Util.UIStrings,
// so this cannot be undefined as long as `report-ui-features.data-i18n` test passes.
const i18nAttr = /** @type {keyof LH.I18NRendererStrings} */ (node.getAttribute('data-i18n'));
node.textContent = Util.i18n.strings[i18nAttr];
}
}
/**
* Define a custom element for <templates> to be extracted from. For example:
* this.setTemplateContext(new DOMParser().parseFromString(htmlStr, 'text/html'))
* @param {ParentNode} context
*/
setTemplateContext(context) {
this._templateContext = context;
}
/**
* @param {{container?: Element, text: string, icon?: string, onClick: () => void}} opts
*/
addButton(opts) {
// report-ui-features doesn't have a reference to the root report el, and PSI has
// 2 reports on the page (and not even attached to DOM when installFeatures is called..)
// so we need a container option to specify where the element should go.
const metricsEl = this._document.querySelector('.lh-audit-group--metrics');
const containerEl = opts.container || metricsEl;
if (!containerEl) return;
let buttonsEl = containerEl.querySelector('.lh-buttons');
if (!buttonsEl) buttonsEl = this._dom.createChildOf(containerEl, 'div', 'lh-buttons');
const classes = [
'lh-button',
];
if (opts.icon) {
classes.push('report-icon');
classes.push(`report-icon--${opts.icon}`);
}
const buttonEl = this._dom.createChildOf(buttonsEl, 'button', classes.join(' '));
buttonEl.textContent = opts.text;
buttonEl.addEventListener('click', opts.onClick);
return buttonEl;
}
/**
* Finds the first scrollable ancestor of `element`. Falls back to the document.
* @param {Element} element
* @return {Node}
*/
_getScrollParent(element) {
const {overflowY} = window.getComputedStyle(element);
const isScrollable = overflowY !== 'visible' && overflowY !== 'hidden';
if (isScrollable) {
return element;
}
if (element.parentElement) {
return this._getScrollParent(element.parentElement);
}
return document;
}
_enableFireworks() {
const scoresContainer = this._dom.find('.lh-scores-container', this._document);
scoresContainer.classList.add('score100');
scoresContainer.addEventListener('click', _ => {
scoresContainer.classList.toggle('fireworks-paused');
});
}
/**
* Fires a custom DOM event on target.
* @param {string} name Name of the event.
* @param {Node=} target DOM node to fire the event on.
* @param {*=} detail Custom data to include.
*/
_fireEventOn(name, target = this._document, detail) {
const event = new CustomEvent(name, detail ? {detail} : undefined);
target.dispatchEvent(event);
}
_setupMediaQueryListeners() {
const mediaQuery = self.matchMedia('(max-width: 500px)');
mediaQuery.addListener(this.onMediaQueryChange);
// Ensure the handler is called on init
this.onMediaQueryChange(mediaQuery);
}
/**
* Handle media query change events.
* @param {MediaQueryList|MediaQueryListEvent} mql
*/
onMediaQueryChange(mql) {
const root = this._dom.find('.lh-root', this._document);
root.classList.toggle('lh-narrow', mql.matches);
}
_setupThirdPartyFilter() {
// Some audits should not display the third party filter option.
const thirdPartyFilterAuditExclusions = [
// These audits deal explicitly with third party resources.
'uses-rel-preconnect',
'third-party-facades',
];
// Some audits should hide third party by default.
const thirdPartyFilterAuditHideByDefault = [
// Only first party resources are actionable.
'legacy-javascript',
];
// Get all tables with a text url column.
const tables = Array.from(this._document.querySelectorAll('table.lh-table'));
const tablesWithUrls = tables
.filter(el =>
el.querySelector('td.lh-table-column--url, td.lh-table-column--source-location'))
.filter(el => {
const containingAudit = el.closest('.lh-audit');
if (!containingAudit) throw new Error('.lh-table not within audit');
return !thirdPartyFilterAuditExclusions.includes(containingAudit.id);
});
tablesWithUrls.forEach((tableEl, index) => {
const rowEls = getTableRows(tableEl);
const thirdPartyRows = this._getThirdPartyRows(rowEls, this.json.finalUrl);
// create input box
const filterTemplate = this._dom.cloneTemplate('#tmpl-lh-3p-filter', this._templateContext);
const filterInput = this._dom.find('input', filterTemplate);
const id = `lh-3p-filter-label--${index}`;
filterInput.id = id;
filterInput.addEventListener('change', e => {
const shouldHideThirdParty = e.target instanceof HTMLInputElement && !e.target.checked;
let even = true;
let rowEl = rowEls[0];
while (rowEl) {
const shouldHide = shouldHideThirdParty && thirdPartyRows.includes(rowEl);
// Iterate subsequent associated sub item rows.
do {
rowEl.classList.toggle('lh-row--hidden', shouldHide);
// Adjust for zebra styling.
rowEl.classList.toggle('lh-row--even', !shouldHide && even);
rowEl.classList.toggle('lh-row--odd', !shouldHide && !even);
rowEl = /** @type {HTMLElement} */ (rowEl.nextElementSibling);
} while (rowEl && rowEl.classList.contains('lh-sub-item-row'));
if (!shouldHide) even = !even;
}
});
this._dom.find('label', filterTemplate).setAttribute('for', id);
this._dom.find('.lh-3p-filter-count', filterTemplate).textContent =
`${thirdPartyRows.length}`;
this._dom.find('.lh-3p-ui-string', filterTemplate).textContent =
Util.i18n.strings.thirdPartyResourcesLabel;
const allThirdParty = thirdPartyRows.length === rowEls.length;
const allFirstParty = !thirdPartyRows.length;
// If all or none of the rows are 3rd party, disable the checkbox.
if (allThirdParty || allFirstParty) {
filterInput.disabled = true;
filterInput.checked = allThirdParty;
}
// Add checkbox to the DOM.
if (!tableEl.parentNode) return; // Keep tsc happy.
tableEl.parentNode.insertBefore(filterTemplate, tableEl);
// Hide third-party rows for some audits by default.
const containingAudit = tableEl.closest('.lh-audit');
if (!containingAudit) throw new Error('.lh-table not within audit');
if (thirdPartyFilterAuditHideByDefault.includes(containingAudit.id) && !allThirdParty) {
filterInput.click();
}
});
}
/**
* @param {Element} el
*/
_setupElementScreenshotOverlay(el) {
const fullPageScreenshot =
this.json.audits['full-page-screenshot'] &&
this.json.audits['full-page-screenshot'].details &&
this.json.audits['full-page-screenshot'].details.type === 'full-page-screenshot' &&
this.json.audits['full-page-screenshot'].details;
if (!fullPageScreenshot) return;
ElementScreenshotRenderer.installOverlayFeature({
dom: this._dom,
reportEl: el,
overlayContainerEl: el,
templateContext: this._templateContext,
fullPageScreenshot,
});
}
/**
* From a table with URL entries, finds the rows containing third-party URLs
* and returns them.
* @param {HTMLElement[]} rowEls
* @param {string} finalUrl
* @return {Array<HTMLElement>}
*/
_getThirdPartyRows(rowEls, finalUrl) {
/** @type {Array<HTMLElement>} */
const thirdPartyRows = [];
const finalUrlRootDomain = Util.getRootDomain(finalUrl);
for (const rowEl of rowEls) {
if (rowEl.classList.contains('lh-sub-item-row')) continue;
const urlItem = rowEl.querySelector('div.lh-text__url');
if (!urlItem) continue;
const datasetUrl = urlItem.dataset.url;
if (!datasetUrl) continue;
const isThirdParty = Util.getRootDomain(datasetUrl) !== finalUrlRootDomain;
if (!isThirdParty) continue;
thirdPartyRows.push(rowEl);
}
return thirdPartyRows;
}
_setupStickyHeaderElements() {
this.topbarEl = this._dom.find('div.lh-topbar', this._document);
this.scoreScaleEl = this._dom.find('div.lh-scorescale', this._document);
this.stickyHeaderEl = this._dom.find('div.lh-sticky-header', this._document);
// Highlighter will be absolutely positioned at first gauge, then transformed on scroll.
this.highlightEl = this._dom.createChildOf(this.stickyHeaderEl, 'div', 'lh-highlighter');
}
/**
* Handle copy events.
* @param {ClipboardEvent} e
*/
onCopy(e) {
// Only handle copy button presses (e.g. ignore the user copying page text).
if (this._copyAttempt && e.clipboardData) {
// We want to write our own data to the clipboard, not the user's text selection.
e.preventDefault();
e.clipboardData.setData('text/plain', JSON.stringify(this.json, null, 2));
this._fireEventOn('lh-log', this._document, {
cmd: 'log', msg: 'Report JSON copied to clipboard',
});
}
this._copyAttempt = false;
}
/**
* Copies the report JSON to the clipboard (if supported by the browser).
*/
onCopyButtonClick() {
this._fireEventOn('lh-analytics', this._document, {
cmd: 'send',
fields: {hitType: 'event', eventCategory: 'report', eventAction: 'copy'},
});
try {
if (this._document.queryCommandSupported('copy')) {
this._copyAttempt = true;
// Note: In Safari 10.0.1, execCommand('copy') returns true if there's
// a valid text selection on the page. See http://caniuse.com/#feat=clipboard.
if (!this._document.execCommand('copy')) {
this._copyAttempt = false; // Prevent event handler from seeing this as a copy attempt.
this._fireEventOn('lh-log', this._document, {
cmd: 'warn', msg: 'Your browser does not support copy to clipboard.',
});
}
}
} catch (/** @type {Error} */ e) {
this._copyAttempt = false;
this._fireEventOn('lh-log', this._document, {cmd: 'log', msg: e.message});
}
}
/**
* Resets the state of page before capturing the page for export.
* When the user opens the exported HTML page, certain UI elements should
* be in their closed state (not opened) and the templates should be unstamped.
*/
_resetUIState() {
this._dropDown.close();
this._dom.resetTemplates();
}
/**
* Handler for tool button.
* @param {Event} e
*/
onDropDownMenuClick(e) {
e.preventDefault();
const el = /** @type {?Element} */ (e.target);
if (!el || !el.hasAttribute('data-action')) {
return;
}
switch (el.getAttribute('data-action')) {
case 'copy':
this.onCopyButtonClick();
break;
case 'print-summary':
this.collapseAllDetails();
this._print();
break;
case 'print-expanded':
this.expandAllDetails();
this._print();
break;
case 'save-json': {
const jsonStr = JSON.stringify(this.json, null, 2);
this._saveFile(new Blob([jsonStr], {type: 'application/json'}));
break;
}
case 'save-html': {
const htmlStr = this.getReportHtml();
try {
this._saveFile(new Blob([htmlStr], {type: 'text/html'}));
} catch (/** @type {Error} */ e) {
this._fireEventOn('lh-log', this._document, {
cmd: 'error', msg: 'Could not export as HTML. ' + e.message,
});
}
break;
}
case 'open-viewer': {
ReportUIFeatures.openTabAndSendJsonReportToViewer(this.json);
break;
}
case 'save-gist': {
this.saveAsGist();
break;
}
case 'toggle-dark': {
this._toggleDarkTheme();
break;
}
}
this._dropDown.close();
}
_print() {
self.print();
}
/**
* Keyup handler for the document.
* @param {KeyboardEvent} e
*/
onKeyUp(e) {
// Ctrl+P - Expands audit details when user prints via keyboard shortcut.
if ((e.ctrlKey || e.metaKey) && e.keyCode === 80) {
this._dropDown.close();
}
}
/**
* The popup's window.name is keyed by version+url+fetchTime, so we reuse/select tabs correctly.
* @param {LH.Result} json
* @protected
*/
static computeWindowNameSuffix(json) {
// @ts-ignore - If this is a v2 LHR, use old `generatedTime`.
const fallbackFetchTime = /** @type {string} */ (json.generatedTime);
const fetchTime = json.fetchTime || fallbackFetchTime;
return `${json.lighthouseVersion}-${json.requestedUrl}-${fetchTime}`;
}
/**
* Opens a new tab to the online viewer and sends the local page's JSON results
* to the online viewer using postMessage.
* @param {LH.Result} json
* @protected
*/
static openTabAndSendJsonReportToViewer(json) {
const windowName = 'viewer-' + this.computeWindowNameSuffix(json);
const url = getAppsOrigin() + '/viewer/';
ReportUIFeatures.openTabAndSendData({lhr: json}, url, windowName);
}
/**
* Opens a new tab to the treemap app and sends the JSON results using URL.fragment
* @param {LH.Result} json
*/
static openTreemap(json) {
const treemapData = json.audits['script-treemap-data'].details;
if (!treemapData) {
throw new Error('no script treemap data found');
}
/** @type {LH.Treemap.Options} */
const treemapOptions = {
lhr: {
requestedUrl: json.requestedUrl,
finalUrl: json.finalUrl,
audits: {
'script-treemap-data': json.audits['script-treemap-data'],
},
configSettings: {
locale: json.configSettings.locale,
},
},
};
const url = getAppsOrigin() + '/treemap/';
const windowName = 'treemap-' + this.computeWindowNameSuffix(json);
ReportUIFeatures.openTabWithUrlData(treemapOptions, url, windowName);
}
/**
* Opens a new tab to an external page and sends data using postMessage.
* @param {{lhr: LH.Result} | LH.Treemap.Options} data
* @param {string} url
* @param {string} windowName
* @protected
*/
static openTabAndSendData(data, url, windowName) {
const origin = new URL(url).origin;
// Chrome doesn't allow us to immediately postMessage to a popup right
// after it's created. Normally, we could also listen for the popup window's
// load event, however it is cross-domain and won't fire. Instead, listen
// for a message from the target app saying "I'm open".
window.addEventListener('message', function msgHandler(messageEvent) {
if (messageEvent.origin !== origin) {
return;
}
if (popup && messageEvent.data.opened) {
popup.postMessage(data, origin);
window.removeEventListener('message', msgHandler);
}
});
const popup = window.open(url, windowName);
}
/**
* Opens a new tab to an external page and sends data via base64 encoded url params.
* @param {{lhr: LH.Result} | LH.Treemap.Options} data
* @param {string} url_
* @param {string} windowName
* @protected
*/
static async openTabWithUrlData(data, url_, windowName) {
const url = new URL(url_);
const gzip = Boolean(window.CompressionStream);
url.hash = await TextEncoding.toBase64(JSON.stringify(data), {
gzip,
});
if (gzip) url.searchParams.set('gzip', '1');
window.open(url.toString(), windowName);
}
/**
* Expands all audit `<details>`.
* Ideally, a print stylesheet could take care of this, but CSS has no way to
* open a `<details>` element.
*/
expandAllDetails() {
const details = this._dom.findAll('.lh-categories details', this._document);
details.map(detail => detail.open = true);
}
/**
* Collapses all audit `<details>`.
* open a `<details>` element.
*/
collapseAllDetails() {
const details = this._dom.findAll('.lh-categories details', this._document);
details.map(detail => detail.open = false);
}
/**
* Sets up listeners to collapse audit `<details>` when the user closes the
* print dialog, all `<details>` are collapsed.
*/
_setUpCollapseDetailsAfterPrinting() {
// FF and IE implement these old events.
if ('onbeforeprint' in self) {
self.addEventListener('afterprint', this.collapseAllDetails);
} else {
// Note: FF implements both window.onbeforeprint and media listeners. However,
// it doesn't matchMedia doesn't fire when matching 'print'.
self.matchMedia('print').addListener(mql => {
if (mql.matches) {
this.expandAllDetails();
} else {
this.collapseAllDetails();
}
});
}
}
/**
* Returns the html that recreates this report.
* @return {string}
* @protected
*/
getReportHtml() {
this._resetUIState();
return this._document.documentElement.outerHTML;
}
/**
* Save json as a gist. Unimplemented in base UI features.
* @protected
*/
saveAsGist() {
throw new Error('Cannot save as gist from base report');
}
/**
* Downloads a file (blob) using a[download].
* @param {Blob|File} blob The file to save.
* @private
*/
_saveFile(blob) {
const filename = getFilenamePrefix({
finalUrl: this.json.finalUrl,
fetchTime: this.json.fetchTime,
});
const ext = blob.type.match('json') ? '.json' : '.html';
const href = URL.createObjectURL(blob);
const a = this._dom.createElement('a');
a.download = `${filename}${ext}`;
a.href = href;
this._document.body.appendChild(a); // Firefox requires anchor to be in the DOM.
a.click();
// cleanup.
this._document.body.removeChild(a);
setTimeout(_ => URL.revokeObjectURL(href), 500);
}
/**
* @private
* @param {boolean} [force]
*/
_toggleDarkTheme(force) {
const el = this._dom.find('.lh-vars', this._document);
// This seems unnecessary, but in DevTools, passing "undefined" as the second
// parameter acts like passing "false".
// https://github.com/ChromeDevTools/devtools-frontend/blob/dd6a6d4153647c2a4203c327c595692c5e0a4256/front_end/dom_extension/DOMExtension.js#L809-L819
if (typeof force === 'undefined') {
el.classList.toggle('dark');
} else {
el.classList.toggle('dark', force);
}
}
_updateStickyHeaderOnScroll() {
// Show sticky header when the score scale begins to go underneath the topbar.
const topbarBottom = this.topbarEl.getBoundingClientRect().bottom;
const scoreScaleTop = this.scoreScaleEl.getBoundingClientRect().top;
const showStickyHeader = topbarBottom >= scoreScaleTop;
// Highlight mini gauge when section is in view.
// In view = the last category that starts above the middle of the window.
const categoryEls = Array.from(this._document.querySelectorAll('.lh-category'));
const categoriesAboveTheMiddle =
categoryEls.filter(el => el.getBoundingClientRect().top - window.innerHeight / 2 < 0);
const highlightIndex =
categoriesAboveTheMiddle.length > 0 ? categoriesAboveTheMiddle.length - 1 : 0;
// Category order matches gauge order in sticky header.
const gaugeWrapperEls = this.stickyHeaderEl.querySelectorAll('.lh-gauge__wrapper');
const gaugeToHighlight = gaugeWrapperEls[highlightIndex];
const origin = gaugeWrapperEls[0].getBoundingClientRect().left;
const offset = gaugeToHighlight.getBoundingClientRect().left - origin;
// Mutate at end to avoid layout thrashing.
this.highlightEl.style.transform = `translate(${offset}px)`;
this.stickyHeaderEl.classList.toggle('lh-sticky-header--visible', showStickyHeader);
}
}
class DropDown {
/**
* @param {DOM} dom
*/
constructor(dom) {
/** @type {DOM} */
this._dom = dom;
/** @type {HTMLElement} */
this._toggleEl; // eslint-disable-line no-unused-expressions
/** @type {HTMLElement} */
this._menuEl; // eslint-disable-line no-unused-expressions
this.onDocumentKeyDown = this.onDocumentKeyDown.bind(this);
this.onToggleClick = this.onToggleClick.bind(this);
this.onToggleKeydown = this.onToggleKeydown.bind(this);
this.onMenuFocusOut = this.onMenuFocusOut.bind(this);
this.onMenuKeydown = this.onMenuKeydown.bind(this);
this._getNextMenuItem = this._getNextMenuItem.bind(this);
this._getNextSelectableNode = this._getNextSelectableNode.bind(this);
this._getPreviousMenuItem = this._getPreviousMenuItem.bind(this);
}
/**
* @param {function(MouseEvent): any} menuClickHandler
*/
setup(menuClickHandler) {
this._toggleEl = this._dom.find('button.lh-tools__button', this._dom.document());
this._toggleEl.addEventListener('click', this.onToggleClick);
this._toggleEl.addEventListener('keydown', this.onToggleKeydown);
this._menuEl = this._dom.find('div.lh-tools__dropdown', this._dom.document());
this._menuEl.addEventListener('keydown', this.onMenuKeydown);
this._menuEl.addEventListener('click', menuClickHandler);
}
close() {
this._toggleEl.classList.remove('active');
this._toggleEl.setAttribute('aria-expanded', 'false');
if (this._menuEl.contains(this._dom.document().activeElement)) {
// Refocus on the tools button if the drop down last had focus
this._toggleEl.focus();
}
this._menuEl.removeEventListener('focusout', this.onMenuFocusOut);
this._dom.document().removeEventListener('keydown', this.onDocumentKeyDown);
}
/**
* @param {HTMLElement} firstFocusElement
*/
open(firstFocusElement) {
if (this._toggleEl.classList.contains('active')) {
// If the drop down is already open focus on the element
firstFocusElement.focus();
} else {
// Wait for drop down transition to complete so options are focusable.
this._menuEl.addEventListener('transitionend', () => {
firstFocusElement.focus();
}, {once: true});
}
this._toggleEl.classList.add('active');
this._toggleEl.setAttribute('aria-expanded', 'true');
this._menuEl.addEventListener('focusout', this.onMenuFocusOut);
this._dom.document().addEventListener('keydown', this.onDocumentKeyDown);
}
/**
* Click handler for tools button.
* @param {Event} e
*/
onToggleClick(e) {
e.preventDefault();
e.stopImmediatePropagation();
if (this._toggleEl.classList.contains('active')) {
this.close();
} else {
this.open(this._getNextMenuItem());
}
}
/**
* Handler for tool button.
* @param {KeyboardEvent} e
*/
onToggleKeydown(e) {
switch (e.code) {
case 'ArrowUp':
e.preventDefault();
this.open(this._getPreviousMenuItem());
break;
case 'ArrowDown':
case 'Enter':
case ' ':
e.preventDefault();
this.open(this._getNextMenuItem());
break;
default:
// no op
}
}
/**
* Handler for tool DropDown.
* @param {KeyboardEvent} e
*/
onMenuKeydown(e) {
const el = /** @type {?HTMLElement} */ (e.target);
switch (e.code) {
case 'ArrowUp':
e.preventDefault();
this._getPreviousMenuItem(el).focus();
break;
case 'ArrowDown':
e.preventDefault();
this._getNextMenuItem(el).focus();
break;
case 'Home':
e.preventDefault();
this._getNextMenuItem().focus();
break;
case 'End':
e.preventDefault();
this._getPreviousMenuItem().focus();
break;
default:
// no op
}
}
/**
* Keydown handler for the document.
* @param {KeyboardEvent} e
*/
onDocumentKeyDown(e) {
if (e.keyCode === 27) { // ESC
this.close();
}
}
/**
* Focus out handler for the drop down menu.
* @param {FocusEvent} e
*/
onMenuFocusOut(e) {
const focusedEl = /** @type {?HTMLElement} */ (e.relatedTarget);
if (!this._menuEl.contains(focusedEl)) {
this.close();
}
}
/**
* @param {Array<Node>} allNodes
* @param {?HTMLElement=} startNode
* @returns {HTMLElement}
*/
_getNextSelectableNode(allNodes, startNode) {
const nodes = allNodes.filter(/** @return {node is HTMLElement} */ (node) => {
if (!(node instanceof HTMLElement)) {
return false;
}
// 'Save as Gist' option may be disabled.
if (node.hasAttribute('disabled')) {
return false;
}
// 'Save as Gist' option may have display none.
if (window.getComputedStyle(node).display === 'none') {
return false;
}
return true;
});
let nextIndex = startNode ? (nodes.indexOf(startNode) + 1) : 0;
if (nextIndex >= nodes.length) {
nextIndex = 0;
}
return nodes[nextIndex];
}
/**
* @param {?HTMLElement=} startEl
* @returns {HTMLElement}
*/
_getNextMenuItem(startEl) {
const nodes = Array.from(this._menuEl.childNodes);
return this._getNextSelectableNode(nodes, startEl);
}
/**
* @param {?HTMLElement=} startEl
* @returns {HTMLElement}
*/
_getPreviousMenuItem(startEl) {
const nodes = Array.from(this._menuEl.childNodes).reverse();
return this._getNextSelectableNode(nodes, startEl);
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = ReportUIFeatures;
} else {
self.ReportUIFeatures = ReportUIFeatures;
}
;
/**
* @license
* Copyright 2017 The Lighthouse Authors. All Rights Reserved.
*
* 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
*
* 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.
*/
'use strict';
/* globals self, Util */
/** @typedef {import('./dom.js')} DOM */
/** @typedef {import('./report-renderer.js')} ReportRenderer */
/** @typedef {import('./details-renderer.js')} DetailsRenderer */
/** @typedef {import('./util.js')} Util */
/** @typedef {'failed'|'warning'|'manual'|'passed'|'notApplicable'} TopLevelClumpId */
class CategoryRenderer {
/**
* @param {DOM} dom
* @param {DetailsRenderer} detailsRenderer
*/
constructor(dom, detailsRenderer) {
/** @type {DOM} */
this.dom = dom;
/** @type {DetailsRenderer} */
this.detailsRenderer = detailsRenderer;
/** @type {ParentNode} */
this.templateContext = this.dom.document();
this.detailsRenderer.setTemplateContext(this.templateContext);
}
/**
* Display info per top-level clump. Define on class to avoid race with Util init.
*/
get _clumpTitles() {
return {
warning: Util.i18n.strings.warningAuditsGroupTitle,
manual: Util.i18n.strings.manualAuditsGroupTitle,
passed: Util.i18n.strings.passedAuditsGroupTitle,
notApplicable: Util.i18n.strings.notApplicableAuditsGroupTitle,
};
}
/**
* @param {LH.ReportResult.AuditRef} audit
* @return {Element}
*/
renderAudit(audit) {
const tmpl = this.dom.cloneTemplate('#tmpl-lh-audit', this.templateContext);
return this.populateAuditValues(audit, tmpl);
}
/**
* Populate an DOM tree with audit details. Used by renderAudit and renderOpportunity
* @param {LH.ReportResult.AuditRef} audit
* @param {DocumentFragment} tmpl
* @return {!Element}
*/
populateAuditValues(audit, tmpl) {
const strings = Util.i18n.strings;
const auditEl = this.dom.find('.lh-audit', tmpl);
auditEl.id = audit.result.id;
const scoreDisplayMode = audit.result.scoreDisplayMode;
if (audit.result.displayValue) {
this.dom.find('.lh-audit__display-text', auditEl).textContent = audit.result.displayValue;
}
const titleEl = this.dom.find('.lh-audit__title', auditEl);
titleEl.appendChild(this.dom.convertMarkdownCodeSnippets(audit.result.title));
const descEl = this.dom.find('.lh-audit__description', auditEl);
descEl.appendChild(this.dom.convertMarkdownLinkSnippets(audit.result.description));
for (const relevantMetric of audit.relevantMetrics || []) {
const adornEl = this.dom.createChildOf(descEl, 'span', 'lh-audit__adorn', {
title: `Relevant to ${relevantMetric.result.title}`,
});
adornEl.textContent = relevantMetric.acronym || relevantMetric.id;
}
if (audit.stackPacks) {
audit.stackPacks.forEach(pack => {
const packElm = this.dom.createElement('div');
packElm.classList.add('lh-audit__stackpack');
const packElmImg = this.dom.createElement('img');
packElmImg.classList.add('lh-audit__stackpack__img');
packElmImg.src = pack.iconDataURL;
packElmImg.alt = pack.title;
packElm.appendChild(packElmImg);
packElm.appendChild(this.dom.convertMarkdownLinkSnippets(pack.description));
this.dom.find('.lh-audit__stackpacks', auditEl)
.appendChild(packElm);
});
}
const header = this.dom.find('details', auditEl);
if (audit.result.details) {
const elem = this.detailsRenderer.render(audit.result.details);
if (elem) {
elem.classList.add('lh-details');
header.appendChild(elem);
}
}
// Add chevron SVG to the end of the summary
this.dom.find('.lh-chevron-container', auditEl).appendChild(this._createChevron());
this._setRatingClass(auditEl, audit.result.score, scoreDisplayMode);
if (audit.result.scoreDisplayMode === 'error') {
auditEl.classList.add(`lh-audit--error`);
const textEl = this.dom.find('.lh-audit__display-text', auditEl);
textEl.textContent = strings.errorLabel;
textEl.classList.add('tooltip-boundary');
const tooltip = this.dom.createChildOf(textEl, 'div', 'tooltip tooltip--error');
tooltip.textContent = audit.result.errorMessage || strings.errorMissingAuditInfo;
} else if (audit.result.explanation) {
const explEl = this.dom.createChildOf(titleEl, 'div', 'lh-audit-explanation');
explEl.textContent = audit.result.explanation;
}
const warnings = audit.result.warnings;
if (!warnings || warnings.length === 0) return auditEl;
// Add list of warnings or singular warning
const summaryEl = this.dom.find('summary', header);
const warningsEl = this.dom.createChildOf(summaryEl, 'div', 'lh-warnings');
this.dom.createChildOf(warningsEl, 'span').textContent = strings.warningHeader;
if (warnings.length === 1) {
warningsEl.appendChild(this.dom.document().createTextNode(warnings.join('')));
} else {
const warningsUl = this.dom.createChildOf(warningsEl, 'ul');
for (const warning of warnings) {
const item = this.dom.createChildOf(warningsUl, 'li');
item.textContent = warning;
}
}
return auditEl;
}
/**
* @return {Element}
*/
_createChevron() {
const chevronTmpl = this.dom.cloneTemplate('#tmpl-lh-chevron', this.templateContext);
const chevronEl = this.dom.find('svg.lh-chevron', chevronTmpl);
return chevronEl;
}
/**
* @param {Element} element DOM node to populate with values.
* @param {number|null} score
* @param {string} scoreDisplayMode
* @return {!Element}
*/
_setRatingClass(element, score, scoreDisplayMode) {
const rating = Util.calculateRating(score, scoreDisplayMode);
element.classList.add(`lh-audit--${scoreDisplayMode.toLowerCase()}`);
if (scoreDisplayMode !== 'informative') {
element.classList.add(`lh-audit--${rating}`);
}
return element;
}
/**
* @param {LH.ReportResult.Category} category
* @param {Record<string, LH.Result.ReportGroup>} groupDefinitions
* @return {DocumentFragment}
*/
renderCategoryHeader(category, groupDefinitions) {
const tmpl = this.dom.cloneTemplate('#tmpl-lh-category-header', this.templateContext);
const gaugeContainerEl = this.dom.find('.lh-score__gauge', tmpl);
const gaugeEl = this.renderScoreGauge(category, groupDefinitions);
gaugeContainerEl.appendChild(gaugeEl);
if (category.description) {
const descEl = this.dom.convertMarkdownLinkSnippets(category.description);
this.dom.find('.lh-category-header__description', tmpl).appendChild(descEl);
}
return tmpl;
}
/**
* Renders the group container for a group of audits. Individual audit elements can be added
* directly to the returned element.
* @param {LH.Result.ReportGroup} group
* @return {Element}
*/
renderAuditGroup(group) {
const groupEl = this.dom.createElement('div', 'lh-audit-group');
const auditGroupHeader = this.dom.createElement('div', 'lh-audit-group__header');
this.dom.createChildOf(auditGroupHeader, 'span', 'lh-audit-group__title')
.textContent = group.title;
if (group.description) {
const descriptionEl = this.dom.convertMarkdownLinkSnippets(group.description);
descriptionEl.classList.add('lh-audit-group__description');
auditGroupHeader.appendChild(descriptionEl);
}
groupEl.appendChild(auditGroupHeader);
return groupEl;
}
/**
* Takes an array of auditRefs, groups them if requested, then returns an
* array of audit and audit-group elements.
* @param {Array<LH.ReportResult.AuditRef>} auditRefs
* @param {Object<string, LH.Result.ReportGroup>} groupDefinitions
* @return {Array<Element>}
*/
_renderGroupedAudits(auditRefs, groupDefinitions) {
// Audits grouped by their group (or under notAGroup).
/** @type {Map<string, Array<LH.ReportResult.AuditRef>>} */
const grouped = new Map();
// Add audits without a group first so they will appear first.
const notAGroup = 'NotAGroup';
grouped.set(notAGroup, []);
for (const auditRef of auditRefs) {
const groupId = auditRef.group || notAGroup;
const groupAuditRefs = grouped.get(groupId) || [];
groupAuditRefs.push(auditRef);
grouped.set(groupId, groupAuditRefs);
}
/** @type {Array<Element>} */
const auditElements = [];
for (const [groupId, groupAuditRefs] of grouped) {
if (groupId === notAGroup) {
// Push not-grouped audits individually.
for (const auditRef of groupAuditRefs) {
auditElements.push(this.renderAudit(auditRef));
}
continue;
}
// Push grouped audits as a group.
const groupDef = groupDefinitions[groupId];
const auditGroupElem = this.renderAuditGroup(groupDef);
for (const auditRef of groupAuditRefs) {
auditGroupElem.appendChild(this.renderAudit(auditRef));
}
auditGroupElem.classList.add(`lh-audit-group--${groupId}`);
auditElements.push(auditGroupElem);
}
return auditElements;
}
/**
* Take a set of audits, group them if they have groups, then render in a top-level
* clump that can't be expanded/collapsed.
* @param {Array<LH.ReportResult.AuditRef>} auditRefs
* @param {Object<string, LH.Result.ReportGroup>} groupDefinitions
* @return {Element}
*/
renderUnexpandableClump(auditRefs, groupDefinitions) {
const clumpElement = this.dom.createElement('div');
const elements = this._renderGroupedAudits(auditRefs, groupDefinitions);
elements.forEach(elem => clumpElement.appendChild(elem));
return clumpElement;
}
/**
* Take a set of audits and render in a top-level, expandable clump that starts
* in a collapsed state.
* @param {Exclude<TopLevelClumpId, 'failed'>} clumpId
* @param {{auditRefs: Array<LH.ReportResult.AuditRef>, description?: string}} clumpOpts
* @return {!Element}
*/
renderClump(clumpId, {auditRefs, description}) {
const clumpTmpl = this.dom.cloneTemplate('#tmpl-lh-clump', this.templateContext);
const clumpElement = this.dom.find('.lh-clump', clumpTmpl);
if (clumpId === 'warning') {
clumpElement.setAttribute('open', '');
}
const summaryInnerEl = this.dom.find('div.lh-audit-group__summary', clumpElement);
summaryInnerEl.appendChild(this._createChevron());
const headerEl = this.dom.find('.lh-audit-group__header', clumpElement);
const title = this._clumpTitles[clumpId];
this.dom.find('.lh-audit-group__title', headerEl).textContent = title;
if (description) {
const descriptionEl = this.dom.convertMarkdownLinkSnippets(description);
descriptionEl.classList.add('lh-audit-group__description');
headerEl.appendChild(descriptionEl);
}
const itemCountEl = this.dom.find('.lh-audit-group__itemcount', clumpElement);
itemCountEl.textContent = `(${auditRefs.length})`;
// Add all audit results to the clump.
const auditElements = auditRefs.map(this.renderAudit.bind(this));
clumpElement.append(...auditElements);
clumpElement.classList.add(`lh-clump--${clumpId.toLowerCase()}`);
return clumpElement;
}
/**
* @param {ParentNode} context
*/
setTemplateContext(context) {
this.templateContext = context;
this.detailsRenderer.setTemplateContext(context);
}
/**
* @param {LH.ReportResult.Category} category
* @param {Record<string, LH.Result.ReportGroup>} groupDefinitions
* @return {DocumentFragment}
*/
renderScoreGauge(category, groupDefinitions) { // eslint-disable-line no-unused-vars
const tmpl = this.dom.cloneTemplate('#tmpl-lh-gauge', this.templateContext);
const wrapper = this.dom.find('a.lh-gauge__wrapper', tmpl);
wrapper.href = `#${category.id}`;
if (Util.isPluginCategory(category.id)) {
wrapper.classList.add('lh-gauge__wrapper--plugin');
}
// Cast `null` to 0
const numericScore = Number(category.score);
const gauge = this.dom.find('.lh-gauge', tmpl);
const gaugeArc = this.dom.find('circle.lh-gauge-arc', gauge);
if (gaugeArc) this._setGaugeArc(gaugeArc, numericScore);
const scoreOutOf100 = Math.round(numericScore * 100);
const percentageEl = this.dom.find('div.lh-gauge__percentage', tmpl);
percentageEl.textContent = scoreOutOf100.toString();
if (category.score === null) {
percentageEl.textContent = '?';
percentageEl.title = Util.i18n.strings.errorLabel;
}
// Render a numerical score if the category has applicable audits, or no audits whatsoever.
if (category.auditRefs.length === 0 || this.hasApplicableAudits(category)) {
wrapper.classList.add(`lh-gauge__wrapper--${Util.calculateRating(category.score)}`);
} else {
wrapper.classList.add(`lh-gauge__wrapper--not-applicable`);
percentageEl.textContent = '-';
percentageEl.title = Util.i18n.strings.notApplicableAuditsGroupTitle;
}
this.dom.find('.lh-gauge__label', tmpl).textContent = category.title;
return tmpl;
}
/**
* Returns true if an LH category has any non-"notApplicable" audits.
* @param {LH.ReportResult.Category} category
* @return {boolean}
*/
hasApplicableAudits(category) {
return category.auditRefs.some(ref => ref.result.scoreDisplayMode !== 'notApplicable');
}
/**
* Define the score arc of the gauge
* Credit to xgad for the original technique: https://codepen.io/xgad/post/svg-radial-progress-meters
* @param {SVGCircleElement} arcElem
* @param {number} percent
*/
_setGaugeArc(arcElem, percent) {
const circumferencePx = 2 * Math.PI * Number(arcElem.getAttribute('r'));
// The rounded linecap of the stroke extends the arc past its start and end.
// First, we tweak the -90deg rotation to start exactly at the top of the circle.
const strokeWidthPx = Number(arcElem.getAttribute('stroke-width'));
const rotationalAdjustmentPercent = 0.25 * strokeWidthPx / circumferencePx;
arcElem.style.transform = `rotate(${-90 + rotationalAdjustmentPercent * 360}deg)`;
// Then, we terminate the line a little early as well.
let arcLengthPx = percent * circumferencePx - strokeWidthPx / 2;
// Special cases. No dot for 0, and full ring if 100
if (percent === 0) arcElem.style.opacity = '0';
if (percent === 1) arcLengthPx = circumferencePx;
arcElem.style.strokeDasharray = `${Math.max(arcLengthPx, 0)} ${circumferencePx}`;
}
/**
* @param {LH.ReportResult.AuditRef} audit
* @return {boolean}
*/
_auditHasWarning(audit) {
return Boolean(audit.result.warnings && audit.result.warnings.length);
}
/**
* Returns the id of the top-level clump to put this audit in.
* @param {LH.ReportResult.AuditRef} auditRef
* @return {TopLevelClumpId}
*/
_getClumpIdForAuditRef(auditRef) {
const scoreDisplayMode = auditRef.result.scoreDisplayMode;
if (scoreDisplayMode === 'manual' || scoreDisplayMode === 'notApplicable') {
return scoreDisplayMode;
}
if (Util.showAsPassed(auditRef.result)) {
if (this._auditHasWarning(auditRef)) {
return 'warning';
} else {
return 'passed';
}
} else {
return 'failed';
}
}
/**
* Renders a set of top level sections (clumps), under a status of failed, warning,
* manual, passed, or notApplicable. The result ends up something like:
*
* failed clump
* ├── audit 1 (w/o group)
* ├── audit 2 (w/o group)
* ├── audit group
* | ├── audit 3
* | └── audit 4
* └── audit group
* ├── audit 5
* └── audit 6
* other clump (e.g. 'manual')
* ├── audit 1
* ├── audit 2
* ├── …
* ⋮
* @param {LH.ReportResult.Category} category
* @param {Object<string, LH.Result.ReportGroup>} [groupDefinitions]
* @return {Element}
*/
render(category, groupDefinitions = {}) {
const element = this.dom.createElement('div', 'lh-category');
this.createPermalinkSpan(element, category.id);
element.appendChild(this.renderCategoryHeader(category, groupDefinitions));
// Top level clumps for audits, in order they will appear in the report.
/** @type {Map<TopLevelClumpId, Array<LH.ReportResult.AuditRef>>} */
const clumps = new Map();
clumps.set('failed', []);
clumps.set('warning', []);
clumps.set('manual', []);
clumps.set('passed', []);
clumps.set('notApplicable', []);
// Sort audits into clumps.
for (const auditRef of category.auditRefs) {
const clumpId = this._getClumpIdForAuditRef(auditRef);
const clump = /** @type {Array<LH.ReportResult.AuditRef>} */ (clumps.get(clumpId)); // already defined
clump.push(auditRef);
clumps.set(clumpId, clump);
}
// Render each clump.
for (const [clumpId, auditRefs] of clumps) {
if (auditRefs.length === 0) continue;
if (clumpId === 'failed') {
const clumpElem = this.renderUnexpandableClump(auditRefs, groupDefinitions);
clumpElem.classList.add(`lh-clump--failed`);
element.appendChild(clumpElem);
continue;
}
const description = clumpId === 'manual' ? category.manualDescription : undefined;
const clumpElem = this.renderClump(clumpId, {auditRefs, description});
element.appendChild(clumpElem);
}
return element;
}
/**
* Create a non-semantic span used for hash navigation of categories
* @param {Element} element
* @param {string} id
*/
createPermalinkSpan(element, id) {
const permalinkEl = this.dom.createChildOf(element, 'span', 'lh-permalink');
permalinkEl.id = id;
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = CategoryRenderer;
} else {
self.CategoryRenderer = CategoryRenderer;
}
;
/**
* @license
* Copyright 2018 The Lighthouse Authors. All Rights Reserved.
*
* 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
*
* 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.
*/
'use strict';
/* globals self, Util, CategoryRenderer */
/** @typedef {import('./dom.js')} DOM */
class PerformanceCategoryRenderer extends CategoryRenderer {
/**
* @param {LH.ReportResult.AuditRef} audit
* @return {!Element}
*/
_renderMetric(audit) {
const tmpl = this.dom.cloneTemplate('#tmpl-lh-metric', this.templateContext);
const element = this.dom.find('.lh-metric', tmpl);
element.id = audit.result.id;
const rating = Util.calculateRating(audit.result.score, audit.result.scoreDisplayMode);
element.classList.add(`lh-metric--${rating}`);
const titleEl = this.dom.find('.lh-metric__title', tmpl);
titleEl.textContent = audit.result.title;
const valueEl = this.dom.find('.lh-metric__value', tmpl);
valueEl.textContent = audit.result.displayValue || '';
const descriptionEl = this.dom.find('.lh-metric__description', tmpl);
descriptionEl.appendChild(this.dom.convertMarkdownLinkSnippets(audit.result.description));
if (audit.result.scoreDisplayMode === 'error') {
descriptionEl.textContent = '';
valueEl.textContent = 'Error!';
const tooltip = this.dom.createChildOf(descriptionEl, 'span');
tooltip.textContent = audit.result.errorMessage || 'Report error: no metric information';
}
return element;
}
/**
* @param {LH.ReportResult.AuditRef} audit
* @param {number} scale
* @return {!Element}
*/
_renderOpportunity(audit, scale) {
const oppTmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity', this.templateContext);
const element = this.populateAuditValues(audit, oppTmpl);
element.id = audit.result.id;
if (!audit.result.details || audit.result.scoreDisplayMode === 'error') {
return element;
}
const details = audit.result.details;
if (details.type !== 'opportunity') {
return element;
}
// Overwrite the displayValue with opportunity's wastedMs
// TODO: normalize this to one tagName.
const displayEl =
this.dom.find('span.lh-audit__display-text, div.lh-audit__display-text', element);
const sparklineWidthPct = `${details.overallSavingsMs / scale * 100}%`;
this.dom.find('div.lh-sparkline__bar', element).style.width = sparklineWidthPct;
displayEl.textContent = Util.i18n.formatSeconds(details.overallSavingsMs, 0.01);
// Set [title] tooltips
if (audit.result.displayValue) {
const displayValue = audit.result.displayValue;
this.dom.find('div.lh-load-opportunity__sparkline', element).title = displayValue;
displayEl.title = displayValue;
}
return element;
}
/**
* Get an audit's wastedMs to sort the opportunity by, and scale the sparkline width
* Opportunities with an error won't have a details object, so MIN_VALUE is returned to keep any
* erroring opportunities last in sort order.
* @param {LH.ReportResult.AuditRef} audit
* @return {number}
*/
_getWastedMs(audit) {
if (audit.result.details && audit.result.details.type === 'opportunity') {
const details = audit.result.details;
if (typeof details.overallSavingsMs !== 'number') {
throw new Error('non-opportunity details passed to _getWastedMs');
}
return details.overallSavingsMs;
} else {
return Number.MIN_VALUE;
}
}
/**
* Get a link to the interactive scoring calculator with the metric values.
* @param {LH.ReportResult.AuditRef[]} auditRefs
* @return {string}
*/
_getScoringCalculatorHref(auditRefs) {
// TODO: filter by !!acronym when dropping renderer support of v7 LHRs.
const metrics = auditRefs.filter(audit => audit.group === 'metrics');
const fci = auditRefs.find(audit => audit.id === 'first-cpu-idle');
const fmp = auditRefs.find(audit => audit.id === 'first-meaningful-paint');
if (fci) metrics.push(fci);
if (fmp) metrics.push(fmp);
/**
* Clamp figure to 2 decimal places
* @param {number} val
* @return {number}
*/
const clampTo2Decimals = val => Math.round(val * 100) / 100;
const metricPairs = metrics.map(audit => {
let value;
if (typeof audit.result.numericValue === 'number') {
value = audit.id === 'cumulative-layout-shift' ?
clampTo2Decimals(audit.result.numericValue) :
Math.round(audit.result.numericValue);
value = value.toString();
} else {
value = 'null';
}
return [audit.acronym || audit.id, value];
});
const paramPairs = [...metricPairs];
if (Util.reportJson) {
paramPairs.push(['device', Util.reportJson.configSettings.formFactor]);
paramPairs.push(['version', Util.reportJson.lighthouseVersion]);
}
const params = new URLSearchParams(paramPairs);
const url = new URL('https://googlechrome.github.io/lighthouse/scorecalc/');
url.hash = params.toString();
return url.href;
}
/**
* @param {LH.ReportResult.Category} category
* @param {Object<string, LH.Result.ReportGroup>} groups
* @param {'PSI'=} environment 'PSI' and undefined are the only valid values
* @return {Element}
* @override
*/
render(category, groups, environment) {
const strings = Util.i18n.strings;
const element = this.dom.createElement('div', 'lh-category');
if (environment === 'PSI') {
const gaugeEl = this.dom.createElement('div', 'lh-score__gauge');
gaugeEl.appendChild(this.renderScoreGauge(category, groups));
element.appendChild(gaugeEl);
} else {
this.createPermalinkSpan(element, category.id);
element.appendChild(this.renderCategoryHeader(category, groups));
}
// Metrics.
const metricAuditsEl = this.renderAuditGroup(groups.metrics);
// Metric descriptions toggle.
const toggleTmpl = this.dom.cloneTemplate('#tmpl-lh-metrics-toggle', this.templateContext);
const _toggleEl = this.dom.find('.lh-metrics-toggle', toggleTmpl);
metricAuditsEl.append(..._toggleEl.childNodes);
const metricAudits = category.auditRefs.filter(audit => audit.group === 'metrics');
const metricsBoxesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metrics-container');
metricAudits.forEach(item => {
metricsBoxesEl.appendChild(this._renderMetric(item));
});
const estValuesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metrics__disclaimer');
const disclaimerEl = this.dom.convertMarkdownLinkSnippets(strings.varianceDisclaimer);
estValuesEl.appendChild(disclaimerEl);
// Add link to score calculator.
const calculatorLink = this.dom.createChildOf(estValuesEl, 'a', 'lh-calclink');
calculatorLink.target = '_blank';
calculatorLink.textContent = strings.calculatorLink;
calculatorLink.href = this._getScoringCalculatorHref(category.auditRefs);
metricAuditsEl.classList.add('lh-audit-group--metrics');
element.appendChild(metricAuditsEl);
// Filmstrip
const timelineEl = this.dom.createChildOf(element, 'div', 'lh-filmstrip-container');
const thumbnailAudit = category.auditRefs.find(audit => audit.id === 'screenshot-thumbnails');
const thumbnailResult = thumbnailAudit && thumbnailAudit.result;
if (thumbnailResult && thumbnailResult.details) {
timelineEl.id = thumbnailResult.id;
const filmstripEl = this.detailsRenderer.render(thumbnailResult.details);
filmstripEl && timelineEl.appendChild(filmstripEl);
}
// Opportunities
const opportunityAudits = category.auditRefs
.filter(audit => audit.group === 'load-opportunities' && !Util.showAsPassed(audit.result))
.sort((auditA, auditB) => this._getWastedMs(auditB) - this._getWastedMs(auditA));
const filterableMetrics = metricAudits.filter(a => !!a.relevantAudits);
// TODO: only add if there are opportunities & diagnostics rendered.
if (filterableMetrics.length) {
this.renderMetricAuditFilter(filterableMetrics, element);
}
if (opportunityAudits.length) {
// Scale the sparklines relative to savings, minimum 2s to not overstate small savings
const minimumScale = 2000;
const wastedMsValues = opportunityAudits.map(audit => this._getWastedMs(audit));
const maxWaste = Math.max(...wastedMsValues);
const scale = Math.max(Math.ceil(maxWaste / 1000) * 1000, minimumScale);
const groupEl = this.renderAuditGroup(groups['load-opportunities']);
const tmpl = this.dom.cloneTemplate('#tmpl-lh-opportunity-header', this.templateContext);
this.dom.find('.lh-load-opportunity__col--one', tmpl).textContent =
strings.opportunityResourceColumnLabel;
this.dom.find('.lh-load-opportunity__col--two', tmpl).textContent =
strings.opportunitySavingsColumnLabel;
const headerEl = this.dom.find('.lh-load-opportunity__header', tmpl);
groupEl.appendChild(headerEl);
opportunityAudits.forEach(item => groupEl.appendChild(this._renderOpportunity(item, scale)));
groupEl.classList.add('lh-audit-group--load-opportunities');
element.appendChild(groupEl);
}
// Diagnostics
const diagnosticAudits = category.auditRefs
.filter(audit => audit.group === 'diagnostics' && !Util.showAsPassed(audit.result))
.sort((a, b) => {
const scoreA = a.result.scoreDisplayMode === 'informative' ? 100 : Number(a.result.score);
const scoreB = b.result.scoreDisplayMode === 'informative' ? 100 : Number(b.result.score);
return scoreA - scoreB;
});
if (diagnosticAudits.length) {
const groupEl = this.renderAuditGroup(groups['diagnostics']);
diagnosticAudits.forEach(item => groupEl.appendChild(this.renderAudit(item)));
groupEl.classList.add('lh-audit-group--diagnostics');
element.appendChild(groupEl);
}
// Passed audits
const passedAudits = category.auditRefs
.filter(audit => (audit.group === 'load-opportunities' || audit.group === 'diagnostics') &&
Util.showAsPassed(audit.result));
if (!passedAudits.length) return element;
const clumpOpts = {
auditRefs: passedAudits,
groupDefinitions: groups,
};
const passedElem = this.renderClump('passed', clumpOpts);
element.appendChild(passedElem);
// Budgets
/** @type {Array<Element>} */
const budgetTableEls = [];
['performance-budget', 'timing-budget'].forEach((id) => {
const audit = category.auditRefs.find(audit => audit.id === id);
if (audit && audit.result.details) {
const table = this.detailsRenderer.render(audit.result.details);
if (table) {
table.id = id;
table.classList.add('lh-audit');
budgetTableEls.push(table);
}
}
});
if (budgetTableEls.length > 0) {
const budgetsGroupEl = this.renderAuditGroup(groups.budgets);
budgetTableEls.forEach(table => budgetsGroupEl.appendChild(table));
budgetsGroupEl.classList.add('lh-audit-group--budgets');
element.appendChild(budgetsGroupEl);
}
return element;
}
/**
* Render the control to filter the audits by metric. The filtering is done at runtime by CSS only
* @param {LH.ReportResult.AuditRef[]} filterableMetrics
* @param {HTMLDivElement} categoryEl
*/
renderMetricAuditFilter(filterableMetrics, categoryEl) {
const metricFilterEl = this.dom.createElement('div', 'lh-metricfilter');
const textEl = this.dom.createChildOf(metricFilterEl, 'span', 'lh-metricfilter__text');
textEl.textContent = Util.i18n.strings.showRelevantAudits;
const filterChoices = /** @type {LH.ReportResult.AuditRef[]} */ ([
({acronym: 'All'}),
...filterableMetrics,
]);
for (const metric of filterChoices) {
const elemId = `metric-${metric.acronym}`;
const radioEl = this.dom.createChildOf(metricFilterEl, 'input', 'lh-metricfilter__radio', {
type: 'radio',
name: 'metricsfilter',
id: elemId,
});
const labelEl = this.dom.createChildOf(metricFilterEl, 'label', 'lh-metricfilter__label', {
for: elemId,
title: metric.result && metric.result.title,
});
labelEl.textContent = metric.acronym || metric.id;
if (metric.acronym === 'All') {
radioEl.checked = true;
labelEl.classList.add('lh-metricfilter__label--active');
}
categoryEl.append(metricFilterEl);
// Toggle class/hidden state based on filter choice.
radioEl.addEventListener('input', _ => {
for (const elem of categoryEl.querySelectorAll('label.lh-metricfilter__label')) {
elem.classList.toggle('lh-metricfilter__label--active', elem.htmlFor === elemId);
}
categoryEl.classList.toggle('lh-category--filtered', metric.acronym !== 'All');
for (const perfAuditEl of categoryEl.querySelectorAll('div.lh-audit')) {
if (metric.acronym === 'All') {
perfAuditEl.hidden = false;
continue;
}
perfAuditEl.hidden = true;
if (metric.relevantAudits && metric.relevantAudits.includes(perfAuditEl.id)) {
perfAuditEl.hidden = false;
}
}
// Hide groups/clumps if all child audits are also hidden.
const groupEls = categoryEl.querySelectorAll('div.lh-audit-group, details.lh-audit-group');
for (const groupEl of groupEls) {
groupEl.hidden = false;
const childEls = Array.from(groupEl.querySelectorAll('div.lh-audit'));
const areAllHidden = !!childEls.length && childEls.every(auditEl => auditEl.hidden);
groupEl.hidden = areAllHidden;
}
});
}
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = PerformanceCategoryRenderer;
} else {
self.PerformanceCategoryRenderer = PerformanceCategoryRenderer;
}
;
/**
* @license
* Copyright 2018 The Lighthouse Authors. All Rights Reserved.
*
* 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
*
* 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.
*/
'use strict';
/* globals self, Util, CategoryRenderer */
class PwaCategoryRenderer extends CategoryRenderer {
/**
* @param {LH.ReportResult.Category} category
* @param {Object<string, LH.Result.ReportGroup>} [groupDefinitions]
* @return {Element}
*/
render(category, groupDefinitions = {}) {
const categoryElem = this.dom.createElement('div', 'lh-category');
this.createPermalinkSpan(categoryElem, category.id);
categoryElem.appendChild(this.renderCategoryHeader(category, groupDefinitions));
const auditRefs = category.auditRefs;
// Regular audits aren't split up into pass/fail/notApplicable clumps, they're
// all put in a top-level clump that isn't expandable/collapsible.
const regularAuditRefs = auditRefs.filter(ref => ref.result.scoreDisplayMode !== 'manual');
const auditsElem = this._renderAudits(regularAuditRefs, groupDefinitions);
categoryElem.appendChild(auditsElem);
// Manual audits are still in a manual clump.
const manualAuditRefs = auditRefs.filter(ref => ref.result.scoreDisplayMode === 'manual');
const manualElem = this.renderClump('manual',
{auditRefs: manualAuditRefs, description: category.manualDescription});
categoryElem.appendChild(manualElem);
return categoryElem;
}
/**
* @param {LH.ReportResult.Category} category
* @param {Record<string, LH.Result.ReportGroup>} groupDefinitions
* @return {DocumentFragment}
*/
renderScoreGauge(category, groupDefinitions) {
// Defer to parent-gauge style if category error.
if (category.score === null) {
return super.renderScoreGauge(category, groupDefinitions);
}
const tmpl = this.dom.cloneTemplate('#tmpl-lh-gauge--pwa', this.templateContext);
const wrapper = this.dom.find('a.lh-gauge--pwa__wrapper', tmpl);
wrapper.href = `#${category.id}`;
// Correct IDs in case multiple instances end up in the page.
const svgRoot = tmpl.querySelector('svg');
if (!svgRoot) throw new Error('no SVG element found in PWA score gauge template');
PwaCategoryRenderer._makeSvgReferencesUnique(svgRoot);
const allGroups = this._getGroupIds(category.auditRefs);
const passingGroupIds = this._getPassingGroupIds(category.auditRefs);
if (passingGroupIds.size === allGroups.size) {
wrapper.classList.add('lh-badged--all');
} else {
for (const passingGroupId of passingGroupIds) {
wrapper.classList.add(`lh-badged--${passingGroupId}`);
}
}
this.dom.find('.lh-gauge__label', tmpl).textContent = category.title;
wrapper.title = this._getGaugeTooltip(category.auditRefs, groupDefinitions);
return tmpl;
}
/**
* Returns the group IDs found in auditRefs.
* @param {Array<LH.ReportResult.AuditRef>} auditRefs
* @return {!Set<string>}
*/
_getGroupIds(auditRefs) {
const groupIds = auditRefs.map(ref => ref.group).filter(/** @return {g is string} */ g => !!g);
return new Set(groupIds);
}
/**
* Returns the group IDs whose audits are all considered passing.
* @param {Array<LH.ReportResult.AuditRef>} auditRefs
* @return {Set<string>}
*/
_getPassingGroupIds(auditRefs) {
const uniqueGroupIds = this._getGroupIds(auditRefs);
// Remove any that have a failing audit.
for (const auditRef of auditRefs) {
if (!Util.showAsPassed(auditRef.result) && auditRef.group) {
uniqueGroupIds.delete(auditRef.group);
}
}
return uniqueGroupIds;
}
/**
* Returns a tooltip string summarizing group pass rates.
* @param {Array<LH.ReportResult.AuditRef>} auditRefs
* @param {Record<string, LH.Result.ReportGroup>} groupDefinitions
* @return {string}
*/
_getGaugeTooltip(auditRefs, groupDefinitions) {
const groupIds = this._getGroupIds(auditRefs);
const tips = [];
for (const groupId of groupIds) {
const groupAuditRefs = auditRefs.filter(ref => ref.group === groupId);
const auditCount = groupAuditRefs.length;
const passedCount = groupAuditRefs.filter(ref => Util.showAsPassed(ref.result)).length;
const title = groupDefinitions[groupId].title;
tips.push(`${title}: ${passedCount}/${auditCount}`);
}
return tips.join(', ');
}
/**
* Render non-manual audits in groups, giving a badge to any group that has
* all passing audits.
* @param {Array<LH.ReportResult.AuditRef>} auditRefs
* @param {Object<string, LH.Result.ReportGroup>} groupDefinitions
* @return {Element}
*/
_renderAudits(auditRefs, groupDefinitions) {
const auditsElem = this.renderUnexpandableClump(auditRefs, groupDefinitions);
// Add a 'badged' class to group if all audits in that group pass.
const passsingGroupIds = this._getPassingGroupIds(auditRefs);
for (const groupId of passsingGroupIds) {
const groupElem = this.dom.find(`.lh-audit-group--${groupId}`, auditsElem);
groupElem.classList.add('lh-badged');
}
return auditsElem;
}
/**
* Alters SVG id references so multiple instances of an SVG element can coexist
* in a single page. If `svgRoot` has a `<defs>` block, gives all elements defined
* in it unique ids, then updates id references (`<use xlink:href="...">`,
* `fill="url(#...)"`) to the altered ids in all descendents of `svgRoot`.
* @param {SVGElement} svgRoot
*/
static _makeSvgReferencesUnique(svgRoot) {
const defsEl = svgRoot.querySelector('defs');
if (!defsEl) return;
const idSuffix = Util.getUniqueSuffix();
const elementsToUpdate = defsEl.querySelectorAll('[id]');
for (const el of elementsToUpdate) {
const oldId = el.id;
const newId = `${oldId}-${idSuffix}`;
el.id = newId;
// Update all <use>s.
const useEls = svgRoot.querySelectorAll(`use[href="#${oldId}"]`);
for (const useEl of useEls) {
useEl.setAttribute('href', `#${newId}`);
}
// Update all fill="url(#...)"s.
const fillEls = svgRoot.querySelectorAll(`[fill="url(#${oldId})"]`);
for (const fillEl of fillEls) {
fillEl.setAttribute('fill', `url(#${newId})`);
}
}
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = PwaCategoryRenderer;
} else {
self.PwaCategoryRenderer = PwaCategoryRenderer;
}
;
/**
* @license
* Copyright 2017 The Lighthouse Authors. All Rights Reserved.
*
* 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
*
* 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.
*/
'use strict';
/**
* @fileoverview The entry point for rendering the Lighthouse report based on the JSON output.
* This file is injected into the report HTML along with the JSON report.
*
* Dummy text for ensuring report robustness: \u003c/script> pre$`post %%LIGHTHOUSE_JSON%%
*/
/** @typedef {import('./category-renderer')} CategoryRenderer */
/** @typedef {import('./dom.js')} DOM */
/* globals self, Util, DetailsRenderer, CategoryRenderer, I18n, PerformanceCategoryRenderer, PwaCategoryRenderer, ElementScreenshotRenderer */
class ReportRenderer {
/**
* @param {DOM} dom
*/
constructor(dom) {
/** @type {DOM} */
this._dom = dom;
/** @type {ParentNode} */
this._templateContext = this._dom.document();
}
/**
* @param {LH.Result} result
* @param {Element} container Parent element to render the report into.
* @return {!Element}
*/
renderReport(result, container) {
this._dom.setLighthouseChannel(result.configSettings.channel || 'unknown');
const report = Util.prepareReportResult(result);
container.textContent = ''; // Remove previous report.
container.appendChild(this._renderReport(report));
return container;
}
/**
* Define a custom element for <templates> to be extracted from. For example:
* this.setTemplateContext(new DOMParser().parseFromString(htmlStr, 'text/html'))
* @param {ParentNode} context
*/
setTemplateContext(context) {
this._templateContext = context;
}
/**
* @param {LH.ReportResult} report
* @return {DocumentFragment}
*/
_renderReportTopbar(report) {
const el = this._dom.cloneTemplate('#tmpl-lh-topbar', this._templateContext);
const metadataUrl = this._dom.find('a.lh-topbar__url', el);
metadataUrl.href = metadataUrl.textContent = report.finalUrl;
metadataUrl.title = report.finalUrl;
return el;
}
/**
* @return {DocumentFragment}
*/
_renderReportHeader() {
const el = this._dom.cloneTemplate('#tmpl-lh-heading', this._templateContext);
const domFragment = this._dom.cloneTemplate('#tmpl-lh-scores-wrapper', this._templateContext);
const placeholder = this._dom.find('.lh-scores-wrapper-placeholder', el);
placeholder.replaceWith(domFragment);
return el;
}
/**
* @param {LH.ReportResult} report
* @return {DocumentFragment}
*/
_renderReportFooter(report) {
const footer = this._dom.cloneTemplate('#tmpl-lh-footer', this._templateContext);
const env = this._dom.find('.lh-env__items', footer);
env.id = 'runtime-settings';
this._dom.find('.lh-env__title', footer).textContent = Util.i18n.strings.runtimeSettingsTitle;
const envValues = Util.getEnvironmentDisplayValues(report.configSettings || {});
const runtimeValues = [
{name: Util.i18n.strings.runtimeSettingsUrl, description: report.finalUrl},
{name: Util.i18n.strings.runtimeSettingsFetchTime,
description: Util.i18n.formatDateTime(report.fetchTime)},
...envValues,
{name: Util.i18n.strings.runtimeSettingsChannel, description: report.configSettings.channel},
{name: Util.i18n.strings.runtimeSettingsUA, description: report.userAgent},
{name: Util.i18n.strings.runtimeSettingsUANetwork, description: report.environment &&
report.environment.networkUserAgent},
{name: Util.i18n.strings.runtimeSettingsBenchmark, description: report.environment &&
report.environment.benchmarkIndex.toFixed(0)},
];
if (report.environment.credits && report.environment.credits['axe-core']) {
runtimeValues.push({
name: Util.i18n.strings.runtimeSettingsAxeVersion,
description: report.environment.credits['axe-core'],
});
}
for (const runtime of runtimeValues) {
if (!runtime.description) continue;
const item = this._dom.cloneTemplate('#tmpl-lh-env__items', env);
this._dom.find('.lh-env__name', item).textContent = runtime.name;
this._dom.find('.lh-env__description', item).textContent = runtime.description;
env.appendChild(item);
}
this._dom.find('.lh-footer__version_issue', footer).textContent = Util.i18n.strings.footerIssue;
this._dom.find('.lh-footer__version', footer).textContent = report.lighthouseVersion;
return footer;
}
/**
* Returns a div with a list of top-level warnings, or an empty div if no warnings.
* @param {LH.ReportResult} report
* @return {Node}
*/
_renderReportWarnings(report) {
if (!report.runWarnings || report.runWarnings.length === 0) {
return this._dom.createElement('div');
}
const container = this._dom.cloneTemplate('#tmpl-lh-warnings--toplevel', this._templateContext);
const message = this._dom.find('.lh-warnings__msg', container);
message.textContent = Util.i18n.strings.toplevelWarningsMessage;
const warnings = this._dom.find('ul', container);
for (const warningString of report.runWarnings) {
const warning = warnings.appendChild(this._dom.createElement('li'));
warning.appendChild(this._dom.convertMarkdownLinkSnippets(warningString));
}
return container;
}
/**
* @param {LH.ReportResult} report
* @param {CategoryRenderer} categoryRenderer
* @param {Record<string, CategoryRenderer>} specificCategoryRenderers
* @return {!DocumentFragment[]}
*/
_renderScoreGauges(report, categoryRenderer, specificCategoryRenderers) {
// Group gauges in this order: default, pwa, plugins.
const defaultGauges = [];
const customGauges = []; // PWA.
const pluginGauges = [];
for (const category of Object.values(report.categories)) {
const renderer = specificCategoryRenderers[category.id] || categoryRenderer;
const categoryGauge = renderer.renderScoreGauge(category, report.categoryGroups || {});
if (Util.isPluginCategory(category.id)) {
pluginGauges.push(categoryGauge);
} else if (renderer.renderScoreGauge === categoryRenderer.renderScoreGauge) {
// The renderer for default categories is just the default CategoryRenderer.
// If the functions are equal, then renderer is an instance of CategoryRenderer.
// For example, the PWA category uses PwaCategoryRenderer, which overrides
// CategoryRenderer.renderScoreGauge, so it would fail this check and be placed
// in the customGauges bucket.
defaultGauges.push(categoryGauge);
} else {
customGauges.push(categoryGauge);
}
}
return [...defaultGauges, ...customGauges, ...pluginGauges];
}
/**
* @param {LH.ReportResult} report
* @return {!DocumentFragment}
*/
_renderReport(report) {
const i18n = new I18n(report.configSettings.locale, {
// Set missing renderer strings to default (english) values.
...Util.UIStrings,
...report.i18n.rendererFormattedStrings,
});
Util.i18n = i18n;
Util.reportJson = report;
const fullPageScreenshot =
report.audits['full-page-screenshot'] && report.audits['full-page-screenshot'].details &&
report.audits['full-page-screenshot'].details.type === 'full-page-screenshot' ?
report.audits['full-page-screenshot'].details : undefined;
const detailsRenderer = new DetailsRenderer(this._dom, {
fullPageScreenshot,
});
const categoryRenderer = new CategoryRenderer(this._dom, detailsRenderer);
categoryRenderer.setTemplateContext(this._templateContext);
/** @type {Record<string, CategoryRenderer>} */
const specificCategoryRenderers = {
performance: new PerformanceCategoryRenderer(this._dom, detailsRenderer),
pwa: new PwaCategoryRenderer(this._dom, detailsRenderer),
};
Object.values(specificCategoryRenderers).forEach(renderer => {
renderer.setTemplateContext(this._templateContext);
});
const headerContainer = this._dom.createElement('div');
headerContainer.appendChild(this._renderReportHeader());
const reportContainer = this._dom.createElement('div', 'lh-container');
const reportSection = this._dom.createElement('div', 'lh-report');
reportSection.appendChild(this._renderReportWarnings(report));
let scoreHeader;
const isSoloCategory = Object.keys(report.categories).length === 1;
if (!isSoloCategory) {
scoreHeader = this._dom.createElement('div', 'lh-scores-header');
} else {
headerContainer.classList.add('lh-header--solo-category');
}
if (scoreHeader) {
const scoreScale = this._dom.cloneTemplate('#tmpl-lh-scorescale', this._templateContext);
const scoresContainer = this._dom.find('.lh-scores-container', headerContainer);
scoreHeader.append(
...this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers));
scoresContainer.appendChild(scoreHeader);
scoresContainer.appendChild(scoreScale);
const stickyHeader = this._dom.createElement('div', 'lh-sticky-header');
stickyHeader.append(
...this._renderScoreGauges(report, categoryRenderer, specificCategoryRenderers));
reportContainer.appendChild(stickyHeader);
}
const categories = reportSection.appendChild(this._dom.createElement('div', 'lh-categories'));
for (const category of Object.values(report.categories)) {
const renderer = specificCategoryRenderers[category.id] || categoryRenderer;
// .lh-category-wrapper is full-width and provides horizontal rules between categories.
// .lh-category within has the max-width: var(--report-width);
const wrapper = renderer.dom.createChildOf(categories, 'div', 'lh-category-wrapper');
wrapper.appendChild(renderer.render(category, report.categoryGroups));
}
const reportFragment = this._dom.createFragment();
const topbarDocumentFragment = this._renderReportTopbar(report);
reportFragment.appendChild(topbarDocumentFragment);
reportFragment.appendChild(reportContainer);
reportContainer.appendChild(headerContainer);
reportContainer.appendChild(reportSection);
reportSection.appendChild(this._renderReportFooter(report));
if (fullPageScreenshot) {
ElementScreenshotRenderer.installFullPageScreenshot(
reportContainer, fullPageScreenshot.screenshot);
}
return reportFragment;
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = ReportRenderer;
} else {
self.ReportRenderer = ReportRenderer;
}
;
/**
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
* 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
* 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.
*/
'use strict';
/* globals self */
// Not named `NBSP` because that creates a duplicate identifier (util.js).
const NBSP2 = '\xa0';
const KiB = 1024;
const MiB = KiB * KiB;
/**
* @template T
*/
class I18n {
/**
* @param {LH.Locale} locale
* @param {T} strings
*/
constructor(locale, strings) {
// When testing, use a locale with more exciting numeric formatting.
if (locale === 'en-XA') locale = 'de';
this._numberDateLocale = locale;
this._numberFormatter = new Intl.NumberFormat(locale);
this._percentFormatter = new Intl.NumberFormat(locale, {style: 'percent'});
this._strings = strings;
}
get strings() {
return this._strings;
}
/**
* Format number.
* @param {number} number
* @param {number=} granularity Number of decimal places to include. Defaults to 0.1.
* @return {string}
*/
formatNumber(number, granularity = 0.1) {
const coarseValue = Math.round(number / granularity) * granularity;
return this._numberFormatter.format(coarseValue);
}
/**
* Format percent.
* @param {number} number 01
* @return {string}
*/
formatPercent(number) {
return this._percentFormatter.format(number);
}
/**
* @param {number} size
* @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
* @return {string}
*/
formatBytesToKiB(size, granularity = 0.1) {
const formatter = this._byteFormatterForGranularity(granularity);
const kbs = formatter.format(Math.round(size / 1024 / granularity) * granularity);
return `${kbs}${NBSP2}KiB`;
}
/**
* @param {number} size
* @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
* @return {string}
*/
formatBytesToMiB(size, granularity = 0.1) {
const formatter = this._byteFormatterForGranularity(granularity);
const kbs = formatter.format(Math.round(size / 1024 ** 2 / granularity) * granularity);
return `${kbs}${NBSP2}MiB`;
}
/**
* @param {number} size
* @param {number=} granularity Controls how coarse the displayed value is, defaults to 1
* @return {string}
*/
formatBytes(size, granularity = 1) {
const formatter = this._byteFormatterForGranularity(granularity);
const kbs = formatter.format(Math.round(size / granularity) * granularity);
return `${kbs}${NBSP2}bytes`;
}
/**
* @param {number} size
* @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
* @return {string}
*/
formatBytesWithBestUnit(size, granularity = 0.1) {
if (size >= MiB) return this.formatBytesToMiB(size, granularity);
if (size >= KiB) return this.formatBytesToKiB(size, granularity);
return this.formatNumber(size, granularity) + '\xa0B';
}
/**
* Format bytes with a constant number of fractional digits, i.e for a granularity of 0.1, 10 becomes '10.0'
* @param {number} granularity Controls how coarse the displayed value is
* @return {Intl.NumberFormat}
*/
_byteFormatterForGranularity(granularity) {
// assume any granularity above 1 will not contain fractional parts, i.e. will never be 1.5
let numberOfFractionDigits = 0;
if (granularity < 1) {
numberOfFractionDigits = -Math.floor(Math.log10(granularity));
}
return new Intl.NumberFormat(this._numberDateLocale, {
...this._numberFormatter.resolvedOptions(),
maximumFractionDigits: numberOfFractionDigits,
minimumFractionDigits: numberOfFractionDigits,
});
}
/**
* @param {number} ms
* @param {number=} granularity Controls how coarse the displayed value is, defaults to 10
* @return {string}
*/
formatMilliseconds(ms, granularity = 10) {
const coarseTime = Math.round(ms / granularity) * granularity;
return coarseTime === 0
? `${this._numberFormatter.format(0)}${NBSP2}ms`
: `${this._numberFormatter.format(coarseTime)}${NBSP2}ms`;
}
/**
* @param {number} ms
* @param {number=} granularity Controls how coarse the displayed value is, defaults to 0.1
* @return {string}
*/
formatSeconds(ms, granularity = 0.1) {
const coarseTime = Math.round(ms / 1000 / granularity) * granularity;
return `${this._numberFormatter.format(coarseTime)}${NBSP2}s`;
}
/**
* Format time.
* @param {string} date
* @return {string}
*/
formatDateTime(date) {
/** @type {Intl.DateTimeFormatOptions} */
const options = {
month: 'short', day: 'numeric', year: 'numeric',
hour: 'numeric', minute: 'numeric', timeZoneName: 'short',
};
// Force UTC if runtime timezone could not be detected.
// See https://github.com/GoogleChrome/lighthouse/issues/1056
// and https://github.com/GoogleChrome/lighthouse/pull/9822
let formatter;
try {
formatter = new Intl.DateTimeFormat(this._numberDateLocale, options);
} catch (err) {
options.timeZone = 'UTC';
formatter = new Intl.DateTimeFormat(this._numberDateLocale, options);
}
return formatter.format(new Date(date));
}
/**
* Converts a time in milliseconds into a duration string, i.e. `1d 2h 13m 52s`
* @param {number} timeInMilliseconds
* @return {string}
*/
formatDuration(timeInMilliseconds) {
let timeInSeconds = timeInMilliseconds / 1000;
if (Math.round(timeInSeconds) === 0) {
return 'None';
}
/** @type {Array<string>} */
const parts = [];
/** @type {Record<string, number>} */
const unitLabels = {
d: 60 * 60 * 24,
h: 60 * 60,
m: 60,
s: 1,
};
Object.keys(unitLabels).forEach(label => {
const unit = unitLabels[label];
const numberOfUnits = Math.floor(timeInSeconds / unit);
if (numberOfUnits > 0) {
timeInSeconds -= numberOfUnits * unit;
parts.push(`${numberOfUnits}\xa0${label}`);
}
});
return parts.join(' ');
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = I18n;
} else {
self.I18n = I18n;
}
;
/**
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
* 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
* 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.
*/
'use strict';
/* global self btoa atob window CompressionStream Response */
const btoa_ = typeof btoa !== 'undefined' ?
btoa :
/** @param {string} str */
(str) => Buffer.from(str).toString('base64');
const atob_ = typeof atob !== 'undefined' ?
atob :
/** @param {string} str */
(str) => Buffer.from(str, 'base64').toString();
/**
* Takes an UTF-8 string and returns a base64 encoded string.
* If gzip is true, the UTF-8 bytes are gzipped before base64'd, using
* CompressionStream (currently only in Chrome), falling back to pako
* (which is only used to encode in our Node tests).
* @param {string} string
* @param {{gzip: boolean}} options
* @return {Promise<string>}
*/
async function toBase64(string, options) {
let bytes = new TextEncoder().encode(string);
if (options.gzip) {
if (typeof CompressionStream !== 'undefined') {
const cs = new CompressionStream('gzip');
const writer = cs.writable.getWriter();
writer.write(bytes);
writer.close();
const compAb = await new Response(cs.readable).arrayBuffer();
bytes = new Uint8Array(compAb);
} else {
/** @type {import('pako')=} */
const pako = window.pako;
bytes = pako.gzip(string);
}
}
let binaryString = '';
// This is ~25% faster than building the string one character at a time.
// https://jsbench.me/2gkoxazvjl
const chunkSize = 5000;
for (let i = 0; i < bytes.length; i += chunkSize) {
binaryString += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
}
return btoa_(binaryString);
}
/**
* @param {string} encoded
* @param {{gzip: boolean}} options
* @return {string}
*/
function fromBase64(encoded, options) {
const binaryString = atob_(encoded);
const bytes = Uint8Array.from(binaryString, c => c.charCodeAt(0));
if (options.gzip) {
/** @type {import('pako')=} */
const pako = window.pako;
return pako.ungzip(bytes, {to: 'string'});
} else {
return new TextDecoder().decode(bytes);
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = {toBase64, fromBase64};
} else {
self.TextEncoding = {toBase64, fromBase64};
}
//# sourceURL=compiled-reportrenderer.js
</script>
<script>window.__LIGHTHOUSE_JSON__ = {"userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/96.0.4664.110 Safari/537.36","environment":{"networkUserAgent":"Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4420.0 Mobile Safari/537.36 Chrome-Lighthouse","hostUserAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/96.0.4664.110 Safari/537.36","benchmarkIndex":1949,"credits":{"axe-core":"4.2.1"}},"lighthouseVersion":"8.0.0","fetchTime":"2022-01-19T08:17:19.449Z","requestedUrl":"http://localhost:8008/congo/samples/emoji/","finalUrl":"http://localhost:8008/congo/samples/emoji/","runWarnings":[],"audits":{"is-on-https":{"id":"is-on-https","title":"Uses HTTPS","description":"All sites should be protected with HTTPS, even ones that don't handle sensitive data. This includes avoiding [mixed content](https://developers.google.com/web/fundamentals/security/prevent-mixed-content/what-is-mixed-content), where some resources are loaded over HTTP despite the initial request being served over HTTPS. HTTPS prevents intruders from tampering with or passively listening in on the communications between your app and your users, and is a prerequisite for HTTP/2 and many new web platform APIs. [Learn more](https://web.dev/is-on-https/).","score":1,"scoreDisplayMode":"binary","details":{"type":"table","headings":[],"items":[]}},"redirects-http":{"id":"redirects-http","title":"Redirects HTTP traffic to HTTPS","description":"If you've already set up HTTPS, make sure that you redirect all HTTP traffic to HTTPS in order to enable secure web features for all your users. [Learn more](https://web.dev/redirects-http/).","score":null,"scoreDisplayMode":"notApplicable"},"service-worker":{"id":"service-worker","title":"Does not register a service worker that controls page and `start_url`","description":"The service worker is the technology that enables your app to use many Progressive Web App features, such as offline, add to homescreen, and push notifications. [Learn more](https://web.dev/service-worker/).","score":0,"scoreDisplayMode":"binary"},"viewport":{"id":"viewport","title":"Has a `\u003cmeta name=\"viewport\">` tag with `width` or `initial-scale`","description":"Add a `\u003cmeta name=\"viewport\">` tag to optimize your app for mobile screens. [Learn more](https://web.dev/viewport/).","score":1,"scoreDisplayMode":"binary","warnings":[]},"first-contentful-paint":{"id":"first-contentful-paint","title":"First Contentful Paint","description":"First Contentful Paint marks the time at which the first text or image is painted. [Learn more](https://web.dev/first-contentful-paint/).","score":0.98,"scoreDisplayMode":"numeric","numericValue":1369,"numericUnit":"millisecond","displayValue":"1.4 s"},"largest-contentful-paint":{"id":"largest-contentful-paint","title":"Largest Contentful Paint","description":"Largest Contentful Paint marks the time at which the largest text or image is painted. [Learn more](https://web.dev/lighthouse-largest-contentful-paint/)","score":1,"scoreDisplayMode":"numeric","numericValue":1500,"numericUnit":"millisecond","displayValue":"1.5 s"},"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://web.dev/first-meaningful-paint/).","score":0.99,"scoreDisplayMode":"numeric","numericValue":1369,"numericUnit":"millisecond","displayValue":"1.4 s"},"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://web.dev/speed-index/).","score":1,"scoreDisplayMode":"numeric","numericValue":1369,"numericUnit":"millisecond","displayValue":"1.4 s"},"screenshot-thumbnails":{"id":"screenshot-thumbnails","title":"Screenshot Thumbnails","description":"This is what the load of your site looked like.","score":null,"scoreDisplayMode":
<script>console.log('window.__LIGHTHOUSE_JSON__', __LIGHTHOUSE_JSON__);</script>
<script>
function __initLighthouseReport__() {
const dom = new DOM(document);
const renderer = new ReportRenderer(dom);
const container = document.querySelector('main');
renderer.renderReport(window.__LIGHTHOUSE_JSON__, container);
// Hook in JS features and page-level event listeners after the report
// is in the document.
const features = new ReportUIFeatures(dom);
features.initFeatures(window.__LIGHTHOUSE_JSON__);
}
window.addEventListener('DOMContentLoaded', __initLighthouseReport__);
document.addEventListener('lh-analytics', e => {
if (window.ga) {
ga(e.detail.cmd, e.detail.fields);
}
});
document.addEventListener('lh-log', e => {
const logger = new Logger(document.querySelector('#lh-log'));
switch (e.detail.cmd) {
case 'log':
logger.log(e.detail.msg);
break;
case 'warn':
logger.warn(e.detail.msg);
break;
case 'error':
logger.error(e.detail.msg);
break;
case 'hide':
logger.hide();
break;
}
});
</script>
</body></html>