<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>webpkit vs libwebp — comparison</title>
<style>
:root {
--bg: #f6f8fa; --panel: #ffffff; --panel-2: #f0f3f7;
--ink: #12161d; --ink-2: #48525f; --ink-3: #7b8798;
--line: #dde3ea; --line-2: #e9edf2;
--accent: #0f8f8f; --accent-ink: #0a6d6d;
--good: #1f9d57; --good-bg: #e2f3e8;
--warn: #b9791f; --warn-bg: #f6ecd9;
--bad: #cf4459; --bad-bg: #f7e0e4;
--track: #e7ecf2;
--shadow: 0 1px 2px rgba(18,22,29,.04), 0 4px 16px rgba(18,22,29,.05);
--mono: ui-monospace, "Cascadia Code", "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0d1015; --panel: #151a21; --panel-2: #1b212a;
--ink: #e7ebf1; --ink-2: #a7b1bf; --ink-3: #6f7b8b;
--line: #262e39; --line-2: #202832;
--accent: #35b9b9; --accent-ink: #56cccc;
--good: #43c07d; --good-bg: #16301f;
--warn: #d59a3f; --warn-bg: #322612;
--bad: #e8697d; --bad-bg: #341a20;
--track: #232b35;
--shadow: 0 1px 2px rgba(0,0,0,.3), 0 6px 20px rgba(0,0,0,.28);
}
}
:root[data-theme="light"] {
--bg: #f6f8fa; --panel: #ffffff; --panel-2: #f0f3f7;
--ink: #12161d; --ink-2: #48525f; --ink-3: #7b8798;
--line: #dde3ea; --line-2: #e9edf2;
--accent: #0f8f8f; --accent-ink: #0a6d6d;
--good: #1f9d57; --good-bg: #e2f3e8; --warn: #b9791f; --warn-bg: #f6ecd9;
--bad: #cf4459; --bad-bg: #f7e0e4; --track: #e7ecf2;
--shadow: 0 1px 2px rgba(18,22,29,.04), 0 4px 16px rgba(18,22,29,.05);
}
:root[data-theme="dark"] {
--bg: #0d1015; --panel: #151a21; --panel-2: #1b212a;
--ink: #e7ebf1; --ink-2: #a7b1bf; --ink-3: #6f7b8b;
--line: #262e39; --line-2: #202832;
--accent: #35b9b9; --accent-ink: #56cccc;
--good: #43c07d; --good-bg: #16301f; --warn: #d59a3f; --warn-bg: #322612;
--bad: #e8697d; --bad-bg: #341a20; --track: #232b35;
--shadow: 0 1px 2px rgba(0,0,0,.3), 0 6px 20px rgba(0,0,0,.28);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0; background: var(--bg); color: var(--ink);
font-family: var(--sans); line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 1060px; margin: 0 auto; padding: clamp(20px, 4vw, 52px) clamp(16px, 4vw, 40px) 80px; }
header { display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; flex-wrap: wrap; margin-bottom: 8px; }
.eyebrow { font-family: var(--mono); font-size: 12px; letter-spacing: .14em; text-transform: uppercase; color: var(--accent-ink); font-weight: 600; }
h1 { font-family: var(--mono); font-size: clamp(26px, 5vw, 40px); font-weight: 650; letter-spacing: -.01em; margin: 6px 0 4px; line-height: 1.08; text-wrap: balance; }
h1 .vs { color: var(--ink-3); font-weight: 400; }
.sub { color: var(--ink-2); max-width: 62ch; font-size: 15px; }
.sub code { font-family: var(--mono); font-size: 13px; }
.toggle { font-family: var(--mono); font-size: 12px; color: var(--ink-2); background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 7px 11px; cursor: pointer; white-space: nowrap; }
.toggle:hover { border-color: var(--accent); color: var(--accent-ink); }
.toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 14px; margin: 26px 0 8px; }
.tile { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 16px 18px; box-shadow: var(--shadow); position: relative; overflow: hidden; }
.tile::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--stripe, var(--accent)); }
.tile .k { font-size: 12.5px; color: var(--ink-2); font-weight: 600; }
.tile .v { font-family: var(--mono); font-size: 25px; font-weight: 650; margin: 6px 0 2px; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }
.tile .note { font-size: 12.5px; color: var(--ink-3); }
.tile.good { --stripe: var(--good); } .tile.warn { --stripe: var(--warn); } .tile.bad { --stripe: var(--bad); }
.tile.good .v { color: var(--good); } .tile.bad .v { color: var(--bad); } .tile.warn .v { color: var(--warn); }
section { margin-top: 40px; }
.sec-head { display: flex; align-items: baseline; gap: 12px; border-bottom: 1px solid var(--line); padding-bottom: 10px; margin-bottom: 20px; flex-wrap: wrap; }
.sec-head h2 { font-family: var(--mono); font-size: 17px; font-weight: 640; margin: 0; }
.sec-head .h-note { font-size: 13px; color: var(--ink-3); margin-left: auto; }
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; box-shadow: var(--shadow); padding: 18px 20px 22px; margin-bottom: 18px; }
.panel h3 { font-family: var(--mono); font-size: 13px; font-weight: 640; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-2); margin: 0 0 4px; }
.panel .cap { font-size: 12.5px; color: var(--ink-3); margin: 0 0 16px; }
.panel .cap code { font-family: var(--mono); font-size: 12px; }
.bars { display: grid; gap: 9px; }
.bar-row { display: grid; grid-template-columns: 72px 1fr 62px; align-items: center; gap: 12px; }
.bar-row .lab { font-family: var(--mono); font-size: 12.5px; color: var(--ink-2); text-align: right; }
.track { position: relative; height: 20px; background: var(--track); border-radius: 5px; overflow: hidden; }
.fill { position: absolute; left: 0; top: 0; bottom: 0; border-radius: 5px 0 0 5px; }
.fill.good { background: linear-gradient(90deg, color-mix(in srgb, var(--good) 78%, transparent), var(--good)); }
.fill.warn { background: linear-gradient(90deg, color-mix(in srgb, var(--warn) 78%, transparent), var(--warn)); }
.fill.bad { background: linear-gradient(90deg, color-mix(in srgb, var(--bad) 78%, transparent), var(--bad)); }
.parity { position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--ink-3); opacity: .8; z-index: 3; }
.parity::after { content: "1×"; position: absolute; top: -15px; left: 50%; transform: translateX(-50%); font-family: var(--mono); font-size: 9.5px; color: var(--ink-3); }
.bar-row .num { font-family: var(--mono); font-size: 13px; font-weight: 600; text-align: right; font-variant-numeric: tabular-nums; }
.num.good { color: var(--good); } .num.warn { color: var(--warn); } .num.bad { color: var(--bad); }
.axis { display: grid; grid-template-columns: 72px 1fr 62px; gap: 12px; margin-top: 8px; }
.axis .ticks { grid-column: 2; display: flex; justify-content: space-between; font-family: var(--mono); font-size: 10px; color: var(--ink-3); }
.grouplab { grid-column: 1 / -1; text-align: left; color: var(--accent-ink); font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; margin: 10px 0 2px; }
.grouplab .m { color: var(--ink-3); }
.grouplab.first { margin-top: 0; }
.scroll { overflow-x: auto; margin: 0 -4px; }
table { border-collapse: collapse; width: 100%; font-size: 13px; min-width: 560px; }
th, td { padding: 8px 10px; text-align: right; border-bottom: 1px solid var(--line-2); font-variant-numeric: tabular-nums; }
th { font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-3); font-weight: 600; border-bottom: 1px solid var(--line); }
td:first-child, th:first-child { text-align: left; font-family: var(--mono); color: var(--ink); }
tbody tr:hover { background: var(--panel-2); }
td.mono { font-family: var(--mono); }
.chip { display: inline-block; font-family: var(--mono); font-size: 11.5px; font-weight: 600; padding: 2px 7px; border-radius: 20px; font-variant-numeric: tabular-nums; }
.chip.good { color: var(--good); background: var(--good-bg); }
.chip.warn { color: var(--warn); background: var(--warn-bg); }
.chip.bad { color: var(--bad); background: var(--bad-bg); }
.legend { display: flex; flex-wrap: wrap; gap: 14px 20px; font-size: 12.5px; color: var(--ink-2); margin-top: 14px; }
.legend span { display: inline-flex; align-items: center; gap: 6px; }
.dot { width: 10px; height: 10px; border-radius: 3px; }
.dot.good { background: var(--good); } .dot.warn { background: var(--warn); } .dot.bad { background: var(--bad); }
.notes { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; }
.note-card { background: var(--panel-2); border: 1px solid var(--line-2); border-radius: 12px; padding: 14px 16px; }
.note-card h4 { font-family: var(--mono); font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--accent-ink); margin: 0 0 6px; }
.note-card p { font-size: 13px; color: var(--ink-2); margin: 0; }
.note-card code { font-family: var(--mono); font-size: 12px; background: var(--panel); padding: 1px 5px; border-radius: 4px; border: 1px solid var(--line); }
footer { margin-top: 40px; padding-top: 18px; border-top: 1px solid var(--line); font-size: 12px; color: var(--ink-3); font-family: var(--mono); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px; }
@media (max-width: 560px) {
.bar-row, .axis { grid-template-columns: 58px 1fr 54px; gap: 8px; }
}
@media (prefers-reduced-motion: no-preference) {
.fill { transition: width .4s ease; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<div>
<div class="eyebrow">Codec benchmark · generated locally</div>
<h1>webpkit <span class="vs">vs</span> libwebp 1.6.0</h1>
<p class="sub">Pure-Rust WebP (<code>#![forbid(unsafe_code)]</code>, zero-dep) measured against the libwebp C reference — size, reconstruction quality, and in-process wall-clock speed across the synthetic content matrix at 256×256.</p>
</div>
<button class="toggle" id="themeBtn" type="button" aria-label="Toggle light or dark theme">◐ theme</button>
</header>
<div class="tiles" id="tiles"></div>
<section>
<div class="sec-head">
<h2>Speed vs libwebp</h2>
<span class="h-note">in-process wall-clock · ratio = ours ÷ libwebp · lower is better</span>
</div>
<div class="panel">
<h3>Decode</h3>
<p class="cap">Both decoders read the same VP8 bitstream. The gap is the price of <code>forbid(unsafe)</code>: libwebp runs hand-tuned SSE2/AVX/NEON; webpkit stays portable scalar Rust. Widest on flat content, where libwebp's SIMD IDCT/prediction gain most.</p>
<div class="bars" id="decodeBars"></div>
<div class="axis"><div class="ticks" id="decodeTicks"></div></div>
</div>
<div class="panel">
<h3>Encode — effort-matched tiers</h3>
<p class="cap">Our <b>Fast / Balanced / Best</b> against libwebp <b>method 1 / 4 / 6</b> at quality 75. Fast is at parity (and can beat libwebp on patterned content); Best is competitive with libwebp's slowest tier; Balanced is the clear soft spot.</p>
<div class="bars" id="encodeBars"></div>
<div class="axis"><div class="ticks" id="encodeTicks"></div></div>
<div class="legend">
<span><i class="dot good"></i> ≤1.15× (parity/faster)</span>
<span><i class="dot warn"></i> 1.15–2×</span>
<span><i class="dot bad"></i> >2× slower</span>
</div>
</div>
</section>
<section>
<div class="sec-head">
<h2>Size & quality vs libwebp</h2>
<span class="h-note">quality 75 · both encoders' output measured in-process</span>
</div>
<div class="panel">
<h3>Lossy — VP8, our Best vs libwebp method 6</h3>
<p class="cap">At the same quality our scale quantizes more aggressively, so read size and PSNR together (not rate-distortion-matched). Size ratio is ours ÷ libwebp bytes; ΔPSNR is ours − libwebp dB.</p>
<div class="scroll">
<table>
<thead>
<tr><th>content</th><th>ours bytes</th><th>libwebp bytes</th><th>size ratio</th><th>ours dB</th><th>libwebp dB</th><th>ΔPSNR</th></tr>
</thead>
<tbody id="lossyBody"></tbody>
</table>
</div>
</div>
<div class="panel">
<h3>Lossless — VP8L, our Best vs libwebp lossless</h3>
<p class="cap">The project's primary focus. Size ratio ours ÷ libwebp; the <b>1×</b> tick is parity, shorter is smaller.</p>
<div class="bars" id="losslessBars"></div>
<div class="axis"><div class="ticks" id="losslessTicks"></div></div>
</div>
</section>
<section>
<div class="sec-head"><h2>Method & caveats</h2></div>
<div class="notes">
<div class="note-card">
<h4>In-process timing</h4>
<p>libwebp linked via <code>libwebp-sys</code> and called in the same process — no CLI startup or file-IO skew. Median of warmed-up iterations.</p>
</div>
<div class="note-card">
<h4>Fairness</h4>
<p>Encode is effort-matched (tier ↔ method) at q75 on identical pixels; decode is timed on each codec's own output (VP8 decode is bit-exact, so cross-decoding is redundant).</p>
</div>
<div class="note-card">
<h4>The SIMD gap</h4>
<p>webpkit is safe scalar Rust by design. The decode gap is structural — closing it means autovectorization-friendly kernels, not a missed micro-op.</p>
</div>
<div class="note-card">
<h4>Reproduce</h4>
<p>Regenerate anytime with <code>just report-vs-libwebp</code>. Wall-clock is machine/thermal-dependent, so this report lives in <code>target/</code>, never committed.</p>
</div>
</div>
</section>
<footer>
<span>webpkit · pure-Rust WebP</span>
<span>corpus: webpkit-samples · libwebp-sys 1.6.0</span>
</footer>
</div>
<script>
(function () {
"use strict";
var DATA = __DATA_JSON__;
var SPEED_MAX = 6.5;
function speedClass(r) { return r <= 1.15 ? "good" : (r <= 2 ? "warn" : "bad"); }
function ticks(el, labels) {
el.innerHTML = labels.map(function (t) { return "<span>" + t + "</span>"; }).join("");
}
function speedRow(name, r) {
var pct = Math.min(r / SPEED_MAX * 100, 100);
var parity = 1 / SPEED_MAX * 100;
var cls = speedClass(r);
return '<div class="bar-row"><div class="lab">' + name + '</div>'
+ '<div class="track"><div class="fill ' + cls + '" style="width:' + pct.toFixed(1) + '%"></div>'
+ '<div class="parity" style="left:' + parity.toFixed(1) + '%"></div></div>'
+ '<div class="num ' + cls + '">' + r.toFixed(2) + '×</div></div>';
}
var decRatios = DATA.map(function (d) { return d.dec.ours / d.dec.lw; });
var decMin = Math.min.apply(null, decRatios), decMax = Math.max.apply(null, decRatios);
var llBest = Math.min.apply(null, DATA.map(function (d) { return d.ll.ob / d.ll.lb; }));
var tiles = [
["good", "Lossless size (VP8L)", "≤ parity", "best case " + Math.round((1 - llBest) * 100) + "% smaller"],
["warn", "Lossy size / quality", "content-split", "wins on smooth, larger on sharp edges"],
["bad", "Decode speed", decMin.toFixed(1) + "–" + decMax.toFixed(1) + "× slower", "safe scalar Rust vs libwebp SIMD"],
["warn", "Encode speed", "Fast ≈ · Best ≈", "Balanced is the soft spot"]
];
document.getElementById("tiles").innerHTML = tiles.map(function (t) {
return '<div class="tile ' + t[0] + '"><div class="k">' + t[1] + '</div>'
+ '<div class="v">' + t[2] + '</div><div class="note">' + t[3] + '</div></div>';
}).join("");
document.getElementById("decodeBars").innerHTML =
DATA.map(function (d) { return speedRow(d.name, d.dec.ours / d.dec.lw); }).join("");
ticks(document.getElementById("decodeTicks"), ["0×", "1×", "2×", "3×", "4×", "5×", "6×"]);
var tierNames = ["Fast", "Balanced", "Best"], tierM = ["1", "4", "6"];
var eh = "";
for (var i = 0; i < 3; i++) {
eh += '<div class="grouplab' + (i === 0 ? ' first' : '') + '">' + tierNames[i]
+ ' <span class="m">↔ libwebp m' + tierM[i] + '</span></div>';
eh += DATA.map(function (d) { return speedRow(d.name, d.enc.ours[i] / d.enc.lw[i]); }).join("");
}
document.getElementById("encodeBars").innerHTML = eh;
ticks(document.getElementById("encodeTicks"), ["0×", "1×", "2×", "3×", "4×", "5×", "6×"]);
document.getElementById("lossyBody").innerHTML = DATA.map(function (d) {
var ratio = d.lossy.ob / d.lossy.lb;
var rc = ratio <= 0.95 ? "good" : (ratio <= 1.1 ? "warn" : "bad");
var dd = d.lossy.op - d.lossy.lp;
var dc = dd >= 0.2 ? "good" : (dd >= -0.5 ? "warn" : "bad");
var dstr = (dd >= 0 ? "+" : "") + dd.toFixed(2);
return "<tr><td>" + d.name + "</td>"
+ '<td class="mono">' + d.lossy.ob.toLocaleString() + "</td>"
+ '<td class="mono">' + d.lossy.lb.toLocaleString() + "</td>"
+ '<td><span class="chip ' + rc + '">' + ratio.toFixed(2) + "×</span></td>"
+ '<td class="mono">' + d.lossy.op.toFixed(2) + "</td>"
+ '<td class="mono">' + d.lossy.lp.toFixed(2) + "</td>"
+ '<td><span class="chip ' + dc + '">' + dstr + "</span></td></tr>";
}).join("");
var llR = DATA.map(function (d) { return d.ll.ob / d.ll.lb; });
var LO = Math.min(0.7, Math.min.apply(null, llR) - 0.03);
var HI = Math.max(1.1, Math.max.apply(null, llR) + 0.03);
document.getElementById("losslessBars").innerHTML = DATA.map(function (d) {
var r = d.ll.ob / d.ll.lb;
var pct = Math.max(0, Math.min((r - LO) / (HI - LO) * 100, 100));
var parity = (1 - LO) / (HI - LO) * 100;
var cls = r <= 0.97 ? "good" : (r <= 1.005 ? "warn" : "bad");
return '<div class="bar-row"><div class="lab">' + d.name + '</div>'
+ '<div class="track"><div class="fill ' + cls + '" style="width:' + pct.toFixed(1) + '%"></div>'
+ '<div class="parity" style="left:' + parity.toFixed(1) + '%"></div></div>'
+ '<div class="num ' + cls + '">' + (r * 100).toFixed(1) + '%</div></div>';
}).join("");
ticks(document.getElementById("losslessTicks"),
[LO.toFixed(2) + "×", "1.0×", HI.toFixed(2) + "×"]);
document.getElementById("themeBtn").addEventListener("click", function () {
var cur = document.documentElement.getAttribute("data-theme");
if (!cur) cur = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
document.documentElement.setAttribute("data-theme", cur === "dark" ? "light" : "dark");
});
})();
</script>
</body>
</html>