<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>zstd WebAssembly example</title>
<style>
:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #18202a;
background: #edf1f4;
}
* {
box-sizing: border-box;
}
body {
min-width: 320px;
margin: 0;
}
main {
width: min(820px, calc(100% - 32px));
margin: 40px auto;
}
header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
h1 {
margin: 0;
font-size: 24px;
font-weight: 700;
letter-spacing: 0;
}
#runtime-status {
margin: 0;
color: #566372;
font-size: 14px;
}
#runtime-status.ready,
#verification.passed {
color: #13734b;
}
#runtime-status.failed,
#verification.failed,
#error {
color: #b4232a;
}
.workspace {
overflow: hidden;
border: 1px solid #cbd3db;
border-radius: 6px;
background: #fff;
}
.editor {
padding: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #354150;
font-size: 13px;
font-weight: 650;
}
textarea {
display: block;
width: 100%;
min-height: 180px;
resize: vertical;
border: 1px solid #aeb8c3;
border-radius: 4px;
padding: 12px;
color: #18202a;
background: #fbfcfd;
font: 14px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
letter-spacing: 0;
}
textarea:focus,
input:focus-visible,
button:focus-visible {
outline: 3px solid rgb(37 99 235 / 25%);
outline-offset: 1px;
border-color: #2563eb;
}
.controls {
display: grid;
grid-template-columns: minmax(180px, 1fr) auto;
align-items: end;
gap: 20px;
margin-top: 16px;
}
.level-header {
display: flex;
justify-content: space-between;
gap: 12px;
}
#level-value {
min-width: 3ch;
color: #18202a;
text-align: right;
}
input[type="range"] {
width: 100%;
accent-color: #2563eb;
}
button {
min-height: 40px;
border: 1px solid #1d4ed8;
border-radius: 4px;
padding: 9px 16px;
color: #fff;
background: #2563eb;
font: inherit;
font-weight: 650;
letter-spacing: 0;
cursor: pointer;
}
button:hover:not(:disabled) {
background: #1d4ed8;
}
button:disabled {
cursor: wait;
opacity: 0.55;
}
.results {
border-top: 1px solid #d8dee5;
background: #f8fafb;
}
.metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
margin: 0;
}
.metric {
min-width: 0;
padding: 16px 20px;
border-right: 1px solid #d8dee5;
}
.metric:last-child {
border-right: 0;
}
dt {
margin-bottom: 5px;
color: #697686;
font-size: 12px;
}
dd {
margin: 0;
overflow-wrap: anywhere;
font-size: 20px;
font-weight: 700;
}
.output {
padding: 0 20px 20px;
}
.output-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-top: 16px;
}
.output-header label,
#verification {
margin: 0;
}
#verification {
font-size: 13px;
font-weight: 650;
}
#compressed-output,
#error {
min-height: 64px;
margin: 8px 0 0;
overflow-wrap: anywhere;
white-space: pre-wrap;
font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
letter-spacing: 0;
}
#compressed-output {
padding: 10px;
border: 1px solid #d8dee5;
border-radius: 4px;
color: #354150;
background: #fff;
}
#error {
min-height: 0;
}
@media (max-width: 560px) {
main {
width: min(100% - 20px, 820px);
margin: 20px auto;
}
header,
.controls {
align-items: stretch;
grid-template-columns: 1fr;
}
header {
display: grid;
}
.metrics {
grid-template-columns: 1fr;
}
.metric {
border-right: 0;
border-bottom: 1px solid #d8dee5;
}
.metric:last-child {
border-bottom: 0;
}
}
</style>
</head>
<body>
<main>
<header>
<h1>zstd WebAssembly</h1>
<p id="runtime-status" role="status">Loading module...</p>
</header>
<section class="workspace" aria-label="zstd example">
<div class="editor">
<label for="source-input">Source text</label>
<textarea id="source-input" spellcheck="false">WebAssembly keeps this zstd example portable across browsers and native runtimes. 你好,zstd!</textarea>
<div class="controls">
<div>
<div class="level-header">
<label for="compression-level">Compression level</label>
<output id="level-value" for="compression-level">3</output>
</div>
<input id="compression-level" type="range" min="-5" max="22" value="3">
</div>
<button id="run" type="button" disabled>Compress and decompress</button>
</div>
</div>
<div class="results" aria-live="polite">
<dl class="metrics">
<div class="metric">
<dt>Input</dt>
<dd id="input-size">-</dd>
</div>
<div class="metric">
<dt>Compressed</dt>
<dd id="compressed-size">-</dd>
</div>
<div class="metric">
<dt>Ratio</dt>
<dd id="compression-ratio">-</dd>
</div>
</dl>
<div class="output">
<div class="output-header">
<label for="compressed-output">Compressed bytes</label>
<p id="verification">Pending</p>
</div>
<pre id="compressed-output">-</pre>
<pre id="error" hidden></pre>
</div>
</div>
</section>
</main>
<script type="module">
const WASM_FILE_NAME = "./debug/zstd.js";
const runtimeStatus = document.querySelector("#runtime-status");
const sourceInput = document.querySelector("#source-input");
const compressionLevel = document.querySelector("#compression-level");
const levelValue = document.querySelector("#level-value");
const runButton = document.querySelector("#run");
const verification = document.querySelector("#verification");
const errorOutput = document.querySelector("#error");
const compressedOutput = document.querySelector("#compressed-output");
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder("utf-8", { fatal: true });
let zstd;
function formatBytes(size) {
return `${size.toLocaleString()} B`;
}
function formatHex(bytes) {
const limit = Math.min(bytes.length, 160);
const values = [];
for (let index = 0; index < limit; index += 1) {
values.push(bytes[index].toString(16).padStart(2, "0"));
}
return `${values.join(" ")}${bytes.length > limit ? " ..." : ""}`;
}
function showError(error) {
verification.textContent = "Failed";
verification.className = "failed";
errorOutput.textContent = error instanceof Error ? error.message : String(error);
errorOutput.hidden = false;
}
function runExample() {
runButton.disabled = true;
errorOutput.hidden = true;
try {
const source = sourceInput.value;
const inputBytes = textEncoder.encode(source);
const compressed = zstd.compress(source, Number(compressionLevel.value));
const restored = textDecoder.decode(zstd.decompress(compressed));
if (restored !== source) {
throw new Error("Round-trip output does not match the source text.");
}
document.querySelector("#input-size").textContent = formatBytes(inputBytes.length);
document.querySelector("#compressed-size").textContent = formatBytes(compressed.length);
document.querySelector("#compression-ratio").textContent = inputBytes.length === 0
? "N/A"
: `${((compressed.length / inputBytes.length) * 100).toFixed(1)}%`;
compressedOutput.textContent = formatHex(compressed);
verification.textContent = "Round-trip passed";
verification.className = "passed";
} catch (error) {
showError(error);
} finally {
runButton.disabled = false;
}
}
compressionLevel.addEventListener("input", () => {
levelValue.value = compressionLevel.value;
});
runButton.addEventListener("click", runExample);
try {
const module = await import(WASM_FILE_NAME);
zstd = await module.default();
runtimeStatus.textContent = "Ready";
runtimeStatus.className = "ready";
runButton.disabled = false;
runExample();
} catch (error) {
runtimeStatus.textContent = "Module failed to load";
runtimeStatus.className = "failed";
showError(error);
}
</script>
</body>
</html>