first working version

pull/8/head
koehr 4 years ago
parent 8ad2a779cd
commit bef1d7342b

1
.gitignore vendored

@ -0,0 +1 @@
node_modules

@ -1,2 +1,44 @@
# 250kb-club
An exclusive members-only club for web pages weighing no more than 250kb. Inspired by Bredley Taunts 1MB.club
An exclusive members-only club for web pages weighing no more than 250kb.
Inspired by [Bredley Taunts 1MB.club](https://1mb.club/).
## But why?
I love the idea of a list of webpages that are still reasonably usable with a slow internet connection. But 1MB is, in my honest opinion, still way too much. Nobody wants to wait 10 seconds — on good days — to load a web site. But a very large chunk of the world population isn't gifted with Gigabit internet connections.
## Adding a web page
Please add a PR in Github that adds a page to `src/pages.json`. If unsure, you can also write an issue mentioning the website. The website will be added after passing the review.
## What are those values?
The values shown in the list are URL, Total Weight, Content Ratio.
Websites listed here are downloaded and analyzed with
(Phantomas)[https://github.com/macbre/phantomas].
The total weight is counted and then the size of actual content is measured
and shown as a ratio.
For example: If a website has a total weight of 100kb and 60kb are the
documents structure, text, images, videos and so on, then the content ratio
is 60%. The rest are extras like CSS, JavaScript and so on. It is hard to
say what a good ratio is but my gut feeling is that everything above 20% is
pretty good already.
## Hacking this page
This page is built with [Svelte](https://svelte.dev). You can clone the repository and run the application in development mode like this:
```
git clone https://github.com/nkoehring/250kb-club.git
cd 250kb-club
yarn
yarn dev
```
And build the page with `yarn build`.
The website analysis is done by `compile-list.js` which reads `pages.txt` and
writes the results to `src/pages.json`.

@ -0,0 +1,58 @@
const fs = require('fs')
const phantomas = require('phantomas')
const pageData = require('./src/pages.json')
const INPUT_FILE = './pages.txt'
const OUTPUT_FILE = './src/pages.json'
const RECHECK_THRESHOLD = 60*60*24*7*1000 // recheck pages older than 1 week
function calcWeights (url, metrics) {
const m = metrics
const extraWeight = m.cssSize + m.jsSize + m.webfontSize + m.otherSize
const contentWeight = m.htmlSize + m.jsonSize + m.imageSize + m.base64Size + m.videoSize
return { url, contentWeight, extraWeight, stamp: Date.now() }
}
async function generateMetrics (urls) {
console.debug('Checking', urls)
const metricsList = []
const keyedPageData = pageData.reduce((acc, page) => {
// stores url/stamp pairs to decide for recheck
acc[page.url] = page
return acc
}, {})
const knownURLs = Object.keys(keyedPageData)
const now = Date.now()
for (const url of urls) {
if (knownURLs.indexOf(url) >= 0) {
if (now - keyedPageData[url].stamp < RECHECK_THRESHOLD) {
console.debug('skipping known URL', url)
metricsList.push(keyedPageData[url]) // push old data to list
continue
}
}
try {
console.debug('fetching and analyzing', url)
const results = await phantomas(url)
metricsList.push(calcWeights(url, results.getMetrics()))
} catch(error) {
console.error(`failed to analyze ${url}`, error)
}
}
try {
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(metricsList))
} catch (err) {
console.error(`ERROR: failed to write results to ${OUTPUT_FILE}`, err)
}
}
try {
const rawString = fs.readFileSync(INPUT_FILE, 'utf8')
const urls = rawString.split('\n').filter(line => line.startsWith('http'))
generateMetrics(urls)
} catch (err) {
console.error(`ERROR: failed to read page list from ${INPUT_FILE}`, err)
}

@ -0,0 +1,23 @@
{
"name": "250kb-club",
"version": "0.1.0",
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"start": "sirv public"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^14.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^8.0.0",
"phantomas": "^2.0.0",
"rollup": "^2.3.4",
"rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-svelte": "^6.0.0",
"rollup-plugin-terser": "^7.0.0",
"svelte": "^3.0.0"
},
"dependencies": {
"sirv-cli": "^1.0.0"
}
}

@ -0,0 +1,14 @@
One URL per line.
Lines that don't start with http:// or https://, as well as duplicates are ignored.
https://koehr.in
https://koehr.tech
https://sjmulder.nl
http://cyberia.host
https://text.npr.org
https://playerone.kevincox.ca
https://dotfilehub.com
https://manpages.bsd.lv
https://danluu.com
https://gtf.io
http://minid.net

@ -0,0 +1,2 @@
/*# sourceMappingURL=bundle.css.map */

@ -0,0 +1,8 @@
{
"version": 3,
"file": "bundle.css",
"sources": [],
"sourcesContent": [],
"names": [],
"mappings": ""
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

@ -0,0 +1,100 @@
body {
font: 16px/1.4 sans-serif;
margin: 0;
padding: 0;
background: white;
color: #333;
}
h1 {
font-size: 2.2em;
line-height: 1.2;
}
body>header,main,body>footer {
max-width: calc(720px - 2em);
width: calc(100% - 2em);
margin: 0 auto;
padding: 0 1em;
}
main {
margin: 3em auto;
}
a,a:visited {
color: currentColor;
text-decoration: underline;
}
select, button {
margin: 0 .5em;
padding: .25em .5em;
border: 2px solid gray;
background: none;
color: currentColor;
font: inherit;
}
footer {
border-top: 1px solid lightgrey;
margin: 3rem auto 0;
font-size: 85%;
}
ol {
list-style: none;
padding: 0;
}
li {
margin-bottom: 1em;
background-color: #0002;
}
.entry {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
padding: .5em .5em 0;
height: 2em;
line-height: 2em;
font-size: 1.3em;
}
.entry > .url {
flex: 1 1 auto;
width: 60%;
overflow: hidden;
text-overflow: ellipsis;
}
.entry > .size, .entry > .ratio {
flex: 0 0 auto;
width: 20%;
text-align: right;
}
.entry-size-bar, .entry-ratio-bar {
height: 0;
margin-bottom: 2px;
border-bottom: 2px solid;
}
.entry-size-bar.highlighted, .entry-ratio-bar.highlighted {
border-bottom-width: 4px;
}
.entry-size-bar {
border-bottom-color: #966;
width: calc(var(--size)/250 * 100%);
}
.entry-ratio-bar {
border-bottom-color: #669;
width: var(--ratio);
}
.float-right {
float: right;
}
@media (prefers-color-scheme: dark) {
body { background: #222; color: white; }
}

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The 250kb Club</title>
<meta name="description" content="An exclusive membership for web pages presenting themselves in no more than 250kb.">
<link rel="icon" href="/favicon.png" type="image/x-icon">
<link rel='stylesheet' href='/global.css'>
<script defer src='/build/bundle.js'></script>
</head>
<body>
<header>
<h1>The 250kb Club</h1>
<p>
The WWW has become a bloated mess. Many pages are loading megabytes of Javascript to show you a few kilobytes of content.
These things are a <strong>cancerous growth</strong> on the web that we should stand up against.
</p>
<p>We can make a difference - no matter how small it may seem. The <em>250kb Club</em> is a collection of web pages that focus on performance, efficiency and accessibility.</p>
<p>
If you'd like to suggest a web page to add to this collection,
<a href="https://github.com/nkoehring/250kb-club" rel="noopener" target="_blank">open a pull request or a ticket in the official Github repository</a>.
The site will be reviewed and, if applicable, added to the list below.
</p>
<p>If your pages exceeds 250kb, you might consider <a href="https://1MB.club" rel="noopener" target="_blank">1MB.club</a> which is the inspiration for this page.</p>
</header>
<main id="members-table">
</main>
<footer>
<p>
Made with &hearts; for a performant web by <a href="https://koehr.in" rel="noopener" target="_blank">Norman Köhring</a>.
Inspired by <a href="https://uglyduck.ca" rel="noopener" target="_blank">Bradley Taunt</a>s <a href="https://1MB.club" rel="noopener" target="_blank">1MB.club</a>
</p>
</footer>
<script data-goatcounter="https://250kb-club.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
</body>
</html>

@ -0,0 +1,83 @@
import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
const isProduction = !process.env.ROLLUP_WATCH;
function serve() {
let server;
function toExit() {
if (server) server.kill(0);
}
return {
writeBundle() {
if (server) return;
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true
});
process.on('SIGTERM', toExit);
process.on('exit', toExit);
}
};
}
export default {
input: 'src/main.js',
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/build/bundle.js'
},
plugins: [
svelte({
// enable run-time checks when not in production
dev: !isProduction,
// we'll extract any component CSS out into
// a separate file - better for performance
css: css => {
css.write('bundle.css');
}
}),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
json({
exclude: ['node_modules/**'],
preferConst: true,
compact: true,
namedExports: false
}),
// In dev mode, call `npm run start` once
// the bundle has been generated
!isProduction && serve(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!isProduction && livereload('public'),
// If we're building for production (npm run build
// instead of npm run dev), minify
isProduction && terser()
],
watch: {
clearScreen: false
}
};

@ -0,0 +1,128 @@
// @ts-check
/** This script modifies the project to support TS code in .svelte files like:
<script lang="ts">
export let name: string;
</script>
As well as validating the code for CI.
*/
/** To work on this script:
rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template
*/
const fs = require("fs")
const path = require("path")
const { argv } = require("process")
const projectRoot = argv[2] || path.join(__dirname, "..")
// Add deps to pkg.json
const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"))
packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, {
"svelte-check": "^1.0.0",
"svelte-preprocess": "^4.0.0",
"@rollup/plugin-typescript": "^6.0.0",
"typescript": "^3.9.3",
"tslib": "^2.0.0",
"@tsconfig/svelte": "^1.0.0"
})
// Add script for checking
packageJSON.scripts = Object.assign(packageJSON.scripts, {
"validate": "svelte-check"
})
// Write the package JSON
fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " "))
// mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too
const beforeMainJSPath = path.join(projectRoot, "src", "main.js")
const afterMainTSPath = path.join(projectRoot, "src", "main.ts")
fs.renameSync(beforeMainJSPath, afterMainTSPath)
// Switch the app.svelte file to use TS
const appSveltePath = path.join(projectRoot, "src", "App.svelte")
let appFile = fs.readFileSync(appSveltePath, "utf8")
appFile = appFile.replace("<script>", '<script lang="ts">')
appFile = appFile.replace("export let name;", 'export let name: string;')
fs.writeFileSync(appSveltePath, appFile)
// Edit rollup config
const rollupConfigPath = path.join(projectRoot, "rollup.config.js")
let rollupConfig = fs.readFileSync(rollupConfigPath, "utf8")
// Edit imports
rollupConfig = rollupConfig.replace(`'rollup-plugin-terser';`, `'rollup-plugin-terser';
import sveltePreprocess from 'svelte-preprocess';
import typescript from '@rollup/plugin-typescript';`)
// Replace name of entry point
rollupConfig = rollupConfig.replace(`'src/main.js'`, `'src/main.ts'`)
// Add preprocess to the svelte config, this is tricky because there's no easy signifier.
// Instead we look for `css:` then the next `}` and add the preprocessor to that
let foundCSS = false
let match
// https://regex101.com/r/OtNjwo/1
const configEditor = new RegExp(/css:.|\n*}/gmi)
while (( match = configEditor.exec(rollupConfig)) != null) {
if (foundCSS) {
const endOfCSSIndex = match.index + 1
rollupConfig = rollupConfig.slice(0, endOfCSSIndex) + ",\n preprocess: sveltePreprocess()," + rollupConfig.slice(endOfCSSIndex);
break
}
if (match[0].includes("css:")) foundCSS = true
}
// Add TypeScript
rollupConfig = rollupConfig.replace(
'commonjs(),',
'commonjs(),\n\t\ttypescript({\n\t\t\tsourceMap: !production,\n\t\t\tinlineSources: !production\n\t\t}),'
);
fs.writeFileSync(rollupConfigPath, rollupConfig)
// Add TSConfig
const tsconfig = `{
"extends": "@tsconfig/svelte/tsconfig.json",
"include": ["src/**/*"],
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
}`
const tsconfigPath = path.join(projectRoot, "tsconfig.json")
fs.writeFileSync(tsconfigPath, tsconfig)
// Delete this script, but not during testing
if (!argv[2]) {
// Remove the script
fs.unlinkSync(path.join(__filename))
// Check for Mac's DS_store file, and if it's the only one left remove it
const remainingFiles = fs.readdirSync(path.join(__dirname))
if (remainingFiles.length === 1 && remainingFiles[0] === '.DS_store') {
fs.unlinkSync(path.join(__dirname, '.DS_store'))
}
// Check if the scripts folder is empty
if (fs.readdirSync(path.join(__dirname)).length === 0) {
// Remove the scripts folder
fs.rmdirSync(path.join(__dirname))
}
}
// Adds the extension recommendation
fs.mkdirSync(path.join(projectRoot, ".vscode"))
fs.writeFileSync(path.join(projectRoot, ".vscode", "extensions.json"), `{
"recommendations": ["svelte.svelte-vscode"]
}
`)
console.log("Converted to TypeScript.")
if (fs.existsSync(path.join(projectRoot, "node_modules"))) {
console.log("\nYou will need to re-run your dependency manager to get started.")
}

@ -0,0 +1,71 @@
<script>
import InfoPopup from './InfoPopup.svelte'
import Link from './Link.svelte'
import data from './pages.json'
const yellowSizeThreshhold = 200
const redSizeThreshhold = 225
const yellowRatioThreshhold = 50
const redRatioThreshhold = 25
const pages = data.map(page => {
const totalWeigth = page.contentWeight + page.extraWeight
const size = Math.round(totalWeigth / 1024)
const ratio = Math.round(page.contentWeight * 100 / totalWeigth)
return { url: page.url, size, ratio }
})
const sortParameters = ['size', 'ratio']
let sortParam = sortParameters[0]
let showInfoPopup = false
$: sortedPages = pages.sort((a, b) => {
return sortParam === 'size' ? a.size - b.size : b.ratio - a.ratio
})
function stripped (url) {
return url.replaceAll(/(^https?:\/\/|\/$)/g, '')
}
function toggleInfo () { showInfoPopup = !showInfoPopup }
</script>
<header>
Sort by:
<select bind:value={sortParam}>
{#each sortParameters as param}
<option value={param}>content-{param}</option>
{/each}
</select>
<button class="float-right" on:click={toggleInfo}>{showInfoPopup ? 'x' : 'How does this work?'}</button>
</header>
{#if showInfoPopup}
<InfoPopup />
{/if}
<ol>
{#each sortedPages as page}
<li style={`--size:${page.size};--ratio:${page.ratio}%`}>
<div class="entry">
<span class="url"><Link href={page.url}>{stripped(page.url)}</Link></span>
<span class="size">{page.size}kb</span>
<span class="ratio">{page.ratio}%</span>
</div>
<div
class="entry-size-bar"
class:highlighted={sortParam === 'size'}
class:yellow={page.size > yellowSizeThreshhold}
class:red={page.size > redSizeThreshhold}
/>
<div
class="entry-ratio-bar"
class:highlighted={sortParam === 'ratio'}
class:yellow={page.ratio > yellowRatioThreshhold}
class:red={page.ratio > redRatioThreshhold}
/>
</li>
{/each}
</ol>

@ -0,0 +1,30 @@
<script>
import Link from './Link.svelte'
</script>
<article id="info-popup">
<header>
<h1>Technical Details</h1>
</header>
<p>
The values shown in the list are URL, Total Weight, Content Ratio.
</p>
<p>
Websites listed here are downloaded and analyzed with
<Link href="https://github.com/macbre/phantomas">Phantomas</Link>.
The total weight is counted and then the size of actual content is measured
and shown as a ratio.
</p>
<p>
For example: If a website has a total weight of 100kb and 60kb are the
documents structure, text, images, videos and so on, then the content ratio
is 60%. The rest are extras like CSS, JavaScript and so on. It is hard to
say what a good ratio is but my gut feeling is that everything above 20% is
pretty good already.
</p>
<p>
<strong>Disclaimer:</strong> Currently, inline scripts and styles are
measured as content due to technical limitations of Phantomas. This will
hopefully be fixed soon.
</p>
</article>

@ -0,0 +1,5 @@
<script>
export let href;
</script>
<a {href} rel="noopener" target="_blank"><slot /></a>

@ -0,0 +1,5 @@
import App from './App.svelte';
var app = new App({ target: document.getElementById('members-table') });
export default app;

@ -0,0 +1 @@
[{"url":"https://koehr.in","contentWeight":23078,"extraWeight":66537,"stamp":1606004545427},{"url":"https://koehr.tech","contentWeight":4964,"extraWeight":20108,"stamp":1606004547391},{"url":"https://sjmulder.nl","contentWeight":2361,"extraWeight":0,"stamp":1606004663706},{"url":"http://cyberia.host","contentWeight":1191,"extraWeight":0,"stamp":1606004664417},{"url":"https://text.npr.org","contentWeight":2760,"extraWeight":0,"stamp":1606004665037},{"url":"https://playerone.kevincox.ca","contentWeight":1904,"extraWeight":42661,"stamp":1606004665881},{"url":"https://dotfilehub.com","contentWeight":961,"extraWeight":1281,"stamp":1606004667422},{"url":"https://manpages.bsd.lv","contentWeight":7045,"extraWeight":1346,"stamp":1606004669823},{"url":"https://danluu.com","contentWeight":2895,"extraWeight":0,"stamp":1606004670441},{"url":"https://gtf.io","contentWeight":2040,"extraWeight":2752,"stamp":1606004671103},{"url":"http://minid.net","contentWeight":4110,"extraWeight":0,"stamp":1606004672171}]

@ -0,0 +1,14 @@
[
{
"url": "https://koehr.in",
"contentWeight": 23078,
"extraWeight": 66538,
"stamp": 1606002516753
},
{
"url": "https://koehr.tech",
"contentWeight": 4964,
"extraWeight": 20108,
"stamp": 1606002519511
}
]

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save