|
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>. |
|
<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>; |
|
} |
|
|