Skip to content

Instantly share code, notes, and snippets.

@developit
Last active May 23, 2019 17:17
Show Gist options
  • Save developit/1cc48cd58318ca6613db7858120dfddc to your computer and use it in GitHub Desktop.
Save developit/1cc48cd58318ca6613db7858120dfddc to your computer and use it in GitHub Desktop.
Lighthouse Viewer in Preact. https://lighthouse-viewer.surge.sh
build
node_modules
*.log
import { Component } from 'preact';
import filesize from 'filesize';
import './style.css';
const Link = props => <a target="_blank" rel="noopener noreferrer" {...props} />;
const Loading = () => <div class="loading">Loading...</div>;
const Section = ({ title, children }) => (
<section>
<header>
<h1 class="title">{title}</h1>
</header>
<output>
{ children[0] || <Loading /> }
</output>
</section>
);
export default class App extends Component {
componentDidMount() {
let font = document.createElement('link');
font.rel = 'stylesheet';
font.href = '//fonts.googleapis.com/css?family=Product+Sans:300,400,600';
document.body.appendChild(font);
fetch('//lighthouse-viewer.appspot.com/data?2017-06-27')
.then(resp => resp.json())
.then(stats => {
this.setState({ stats });
});
}
render(_, { stats }) {
return (
<div id="wrapper">
<Section title="Desktop">
{stats && <Stats stats={stats.desktop} />}
</Section>
<Section title="Mobile">
{stats && <Stats stats={stats.mobile} />}
</Section>
<Section title={<Link href="https://developers.google.com/web/tools/lighthouse/">Lighthouse</Link>}>
{stats && <LHResults stats={stats.lighthouse} />}
</Section>
<footer>
<div>
<b>Median</b> values for the top <b>475k+</b> websites.
Snapshot is from {stats && stats.latestFetchDate}, provided by <Link href="http://httparchive.org/">httparchive.org</Link>.&nbsp;
<Link href="https://github.com/ebidel/lighthouse-httparchive">Source on Github</Link>.
</div>
</footer>
</div>
);
}
}
const KEYS_TO_LABEL = {
total_bytes: 'Total',
img_bytes: 'Images',
html_doc_bytes: 'Size of main page',
html_bytes: 'HTML',
css_bytes: 'CSS',
font_bytes: 'Fonts',
js_bytes: 'JS',
js_requests: 'JS',
css_requests: 'CSS',
img_requests: 'Images',
html_requests: 'HTML',
num_dom_elements: '# of DOM nodes',
render_start: 'First paint (ms)',
speed_index: 'Page Speed Index',
pwaScore: 'Mobile score',
bestPracticesScore: 'Mobile score',
a11yScore: 'Mobile score',
perfScore: 'Mobile score'
};
const Card = ({ label, values }) => (
<div class="scorecard shadow">
<div class="scorecard-title">{label}</div>
<div class="scorecard-rows" data-name={label.toLowerCase()}>
{ values.map( ({ label, value }) => (
<div class="scorecard-row">
<span class="scorecard-row-title">{label}</span>
<h1 class="scorecard-row-score" title={value.raw}>{value.formatted}</h1>
</div>
)) }
</div>
</div>
);
function formatBytesToKb(bytes) {
return {
raw: bytes,
formatted: filesize(bytes, { base: 10, round: 1 })
};
}
function formatNumber(num) {
return { raw: num, formatted: Math.round(num) };
}
function sortArrayOfObjectsByValues(stats) {
const sortedEntries = stats;
sortedEntries.sort((a, b) => {
if (a.value.raw < b.value.raw) {
return -1;
}
if (a.value.raw > b.value.raw) {
return 1;
}
return 0;
});
return sortedEntries.reverse();
}
function Stats({ stats }) {
const KEYS = { size: 'Weight', requests: 'Requests', perf: 'Page performance' };
const groups = {
[KEYS.size]: [],
[KEYS.requests]: [],
[KEYS.perf]: []
};
// Construct formatted object for rendering.
for (const [key, val] of Object.entries(stats)) {
const label = KEYS_TO_LABEL[key];
if (!label) {
continue;
}
if (key === 'html_doc_bytes') {
groups[KEYS.perf].push({ label, value: formatBytesToKb(val) });
}
else if (key.endsWith('bytes')) {
groups[KEYS.size].push({ label, value: formatBytesToKb(val) });
}
else if (key.endsWith('_requests')) {
groups[KEYS.requests].push({ label, value: formatNumber(val) });
}
else {
groups[KEYS.perf].push({ label, value: formatNumber(val) });
}
}
let cards = [];
// Create a card for each group.
// eslint-disable-next-line prefer-const
for (let [label, values] of Object.entries(groups)) {
// Sort some of the groups, largest -> smallest.
if (label === KEYS.size || label === KEYS.requests) {
values = sortArrayOfObjectsByValues(values);
}
cards.push(<Card label={label} values={values} />);
}
return <div class="data-container">{cards}</div>;
}
function LHResults({ stats }) {
const KEYS = {
perfScore: 'Performance',
pwaScore: 'PWA',
a11yScore: 'Accessibility',
bestPracticesScore: 'Best Practices'
};
const groups = {
[KEYS.perfScore]: [],
[KEYS.pwaScore]: [],
[KEYS.a11yScore]: [],
[KEYS.bestPracticesScore]: []
};
// Construct formatted object for rendering.
for (const [key, val] of Object.entries(stats)) {
const label = KEYS_TO_LABEL[key];
if (!label) {
continue;
}
groups[KEYS[key]].push({ label, value: formatNumber(val) });
}
let cards = [];
for (let [label, values] of Object.entries(groups)) {
cards.push(<Card label={label} values={values} />);
}
return <div class="data-container">{cards}</div>;
}
{
"name": "Lighthouse Viewer",
"short_name": "Lighthouse Viewer",
"start_url": "/",
"display": "standalone",
"background_color": "#fafafa",
"theme_color": "#eceff1",
"icons": [{
"src": "/icon.png",
"type": "image/png",
"sizes": "512x512"
}]
}
{
"name": "lighthouse-viewer",
"scripts": {
"dev": "preact watch",
"build": "preact build",
"start": "preact build && preact serve",
"test": "eslint . && preact test"
},
"eslintConfig": {
"extends": "eslint-config-synacor"
},
"devDependencies": {
"eslint": "^4.1.1",
"eslint-config-synacor": "^1.0.1",
"preact-cli": "^1.3.0"
},
"dependencies": {
"filesize": "^3.5.10",
"preact": "^8.1.0"
}
}
:root {
--padding: 16px;
--title-color: var(--md-blue-grey-700);
--md-blue-grey-50: #ECEFF1;
--md-blue-grey-100: #CFD8DC;
--md-blue-grey-200: #B0BEC5;
--md-blue-grey-300: #90A4AE;
--md-blue-grey-400: #78909C;
--md-blue-grey-500: #607D8B;
--md-blue-grey-700: #455A64;
--md-blue-grey-900: #263238;
}
* {
box-sizing: border-box;
}
html, body {
margin: 0;
height: 100%;
}
body {
padding: var(--padding);
font-family: "Product Sans", "Roboto", sans-serif;
font-weight: 300;
background-color: #fafafa;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
h1, h2, h3, h4 {
padding: 0;
margin: 0;
font-weight: inherit;
}
#wrapper {
max-width: 1000px;
display: flex;
}
header {
display: flex;
align-items: center;
margin-bottom: var(--padding);
}
.title {
font-size: 40px;
text-transform: uppercase;
color: var(--title-color);
margin-right: var(--padding);
flex: 1 0 auto;
text-align: center;
}
.title a {
text-decoration: none;
}
h1 {
font-size: 32px;
}
a {
color: currentcolor;
}
section {
margin-bottom: 51px; /* height of bottom footer */
width: 33.33%;
}
section:nth-child(odd) {
margin: 0 calc(var(--padding) * 2);
}
footer {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
padding: var(--padding);
background-color: #eee;
color: var(--md-blue-grey-300);
text-align: center;
}
.shadow {
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12),
0 3px 1px -2px rgba(0, 0, 0, 0.2);
}
.data-container {
display: flex;
flex-wrap: wrap;
}
.scorecard {
min-width: 150px;
flex: 1 1 auto;
padding: calc(var(--padding) / 2);
padding-top: calc(39px + var(--padding) / 2); /* height of top bar + padding */
border-radius: 3px;
margin-bottom: var(--padding);
background-color: #fff;
position: relative;
}
.scorecard-title {
text-transform: uppercase;
letter-spacing: 0.03em;
font-size: 16px;
padding: calc(var(--padding) / 2) 0;
color: var(--md-blue-grey-50);
background: var(--md-blue-grey-500);
position: absolute;
top: 0;
right: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}
.scorecard-rows {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.scorecard-row {
padding: 0 calc(var(--padding) / 2);
display: flex;
align-items: center;
flex-direction: column;
flex: 1;
white-space: nowrap;
}
.scorecard-rows[data-name$="performance"] .scorecard-row-score,
.scorecard-rows[data-name$="weight"] .scorecard-row-score {
margin-bottom: calc(var(--padding) / 2);
}
.scorecard-row-title {
color: var(--md-blue-grey-400);
}
.scorecard-row-score {
color: var(--title-color);
}
.loading {
color: #ccc;
text-align: center;
padding: calc(var(--padding) * 2);
}
@media (max-width: 678px) {
#wrapper {
justify-content: center;
flex-wrap: wrap;
}
section {
width: 100%;
}
section:last-child {
margin-bottom: 140px;
}
.scorecard.sidebyside {
margin-right: 0;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment