Image Compression & Export Get Certified
Once an image is on a canvas you can export it in different formats and quality levels using toBlob() or toDataURL(). This is how browser-based image compressors, resize tools, and upload optimizers work.
Exporting a Canvas as a Blob
canvas.toBlob() is the modern way to get a compressed file from a canvas. The result is delivered asynchronously through a callback. In this lesson the examples draw a sample picture with makePhoto() and export it with canvasToBlob(), both placed at the bottom of each example. canvasToBlob() is a stand-in that returns the same kind of Blob immediately (it is built on toDataURL()), so the real file sizes can be printed and the pictures shown.
What this code does: Draws a 400×300 sample picture, exports it as a JPEG at quality 0.8 and prints the real format and size of the resulting file. The exported JPEG is then displayed below the text output, with its size in the caption. The comment shows how the same export looks with the real, asynchronous toBlob().
const canvas = makePhoto(400, 300);
// Real page (asynchronous):
// canvas.toBlob((blob) => { /* use blob here */ }, "image/jpeg", 0.8);
// Here canvasToBlob() gives us the same kind of Blob immediately.
const blob = canvasToBlob(canvas, "image/jpeg", 0.8);
console.log("Format:", blob.type);
console.log("Size:", (blob.size / 1024).toFixed(1), "KB");
console.log("Canvas pixels:", canvas.width + "x" + canvas.height);
showBlob("Exported JPEG at quality 0.8", blob);
// ===== Helpers below are not part of the lesson =====
function showPreview(label, node) {
setTimeout(() => {
const box = document.createElement("div");
box.className = "demo-preview-box";
box.innerHTML = "<p class='demo-preview-label'>" + label + "</p>";
box.appendChild(node);
document.getElementById("previewContent").appendChild(box);
}, 0);
}
function makePhoto(width, height) {
const c = document.createElement("canvas");
c.width = width;
c.height = height;
const g = c.getContext("2d");
const sky = g.createLinearGradient(0, 0, 0, height);
sky.addColorStop(0, "#1e3a8a");
sky.addColorStop(0.6, "#f59e0b");
sky.addColorStop(1, "#7c2d12");
g.fillStyle = sky;
g.fillRect(0, 0, width, height);
g.fillStyle = "#fde68a";
g.beginPath();
g.arc(width * 0.7, height * 0.35, height * 0.12, 0, Math.PI * 2);
g.fill();
g.fillStyle = "#14532d";
for (let i = 0; i < 6; i++) {
g.beginPath();
g.arc(width * (i / 5), height * 0.9, height * 0.22, 0, Math.PI * 2);
g.fill();
}
let seed = 7;
for (let i = 0; i < (width * height) / 40; i++) {
seed = (seed * 16807) % 2147483647;
const x = seed % width;
seed = (seed * 16807) % 2147483647;
const y = seed % height;
g.fillStyle = "rgba(255, 255, 255, " + ((seed % 100) / 600) + ")";
g.fillRect(x, y, 2, 2);
}
return c;
}
function canvasToBlob(canvas, type, quality) {
const dataUrl = canvas.toDataURL(type, quality);
const mime = dataUrl.slice(5, dataUrl.indexOf(";"));
const binary = atob(dataUrl.split(",")[1]);
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
return new Blob([bytes], { type: mime });
}
function showBlob(label, blob) {
const img = document.createElement("img");
img.className = "demo-export-image";
img.style.cssText = "max-width:100%;border-radius:6px;display:block;";
img.onload = () => URL.revokeObjectURL(img.src);
img.src = URL.createObjectURL(blob);
showPreview(label + " (" + (blob.size / 1024).toFixed(1) + " KB, " + blob.type + ")", img);
}
How Quality Affects File Size
Lowering the quality value (0–1) reduces file size for JPEG and WebP, with a trade-off in visual fidelity.
What this code does: Exports the same picture as a JPEG at three quality levels (0.9, 0.5 and 0.1), prints each real file size and shows all three exported images plus a bar chart of the sizes. Look at the pictures as well as the numbers: at 0.1 the file is tiny but blocky artefacts become visible, which is the trade-off you are choosing.
const canvas = makePhoto(400, 300);
const qualities = [0.9, 0.5, 0.1];
const results = [];
qualities.forEach((quality) => {
const blob = canvasToBlob(canvas, "image/jpeg", quality);
console.log("JPEG quality " + quality + ":", (blob.size / 1024).toFixed(1), "KB");
results.push({ label: "quality " + quality, size: blob.size });
showBlob("JPEG quality " + quality, blob);
});
showBars("File size by quality", results);
// ===== Helpers below are not part of the lesson =====
function showPreview(label, node) {
setTimeout(() => {
const box = document.createElement("div");
box.className = "demo-preview-box";
box.innerHTML = "<p class='demo-preview-label'>" + label + "</p>";
box.appendChild(node);
document.getElementById("previewContent").appendChild(box);
}, 0);
}
function makePhoto(width, height) {
const c = document.createElement("canvas");
c.width = width;
c.height = height;
const g = c.getContext("2d");
const sky = g.createLinearGradient(0, 0, 0, height);
sky.addColorStop(0, "#1e3a8a");
sky.addColorStop(0.6, "#f59e0b");
sky.addColorStop(1, "#7c2d12");
g.fillStyle = sky;
g.fillRect(0, 0, width, height);
g.fillStyle = "#fde68a";
g.beginPath();
g.arc(width * 0.7, height * 0.35, height * 0.12, 0, Math.PI * 2);
g.fill();
g.fillStyle = "#14532d";
for (let i = 0; i < 6; i++) {
g.beginPath();
g.arc(width * (i / 5), height * 0.9, height * 0.22, 0, Math.PI * 2);
g.fill();
}
let seed = 7;
for (let i = 0; i < (width * height) / 40; i++) {
seed = (seed * 16807) % 2147483647;
const x = seed % width;
seed = (seed * 16807) % 2147483647;
const y = seed % height;
g.fillStyle = "rgba(255, 255, 255, " + ((seed % 100) / 600) + ")";
g.fillRect(x, y, 2, 2);
}
return c;
}
function canvasToBlob(canvas, type, quality) {
const dataUrl = canvas.toDataURL(type, quality);
const mime = dataUrl.slice(5, dataUrl.indexOf(";"));
const binary = atob(dataUrl.split(",")[1]);
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
return new Blob([bytes], { type: mime });
}
function showBlob(label, blob) {
const img = document.createElement("img");
img.className = "demo-export-image";
img.style.cssText = "max-width:100%;border-radius:6px;display:block;";
img.onload = () => URL.revokeObjectURL(img.src);
img.src = URL.createObjectURL(blob);
showPreview(label + " (" + (blob.size / 1024).toFixed(1) + " KB, " + blob.type + ")", img);
}
function showBars(title, items) {
const wrap = document.createElement("div");
wrap.className = "demo-bars";
wrap.style.cssText = "font:13px sans-serif;color:#0f172a;max-width:420px;";
const max = Math.max(...items.map((item) => item.size));
items.forEach((item) => {
const row = document.createElement("div");
row.className = "demo-bar-row";
row.style.cssText = "display:flex;align-items:center;gap:8px;margin:4px 0;";
row.innerHTML = "<span style='display:inline-block;width:95px'>" + item.label + "</span>"
+ "<div style='flex:1;background:#e2e8f0;border-radius:4px'>"
+ "<div style='height:16px;border-radius:4px;background:#4f46e5;width:" + (item.size / max * 100) + "%'></div></div>"
+ "<span style='display:inline-block;width:75px;text-align:right'>" + (item.size / 1024).toFixed(1) + " KB</span>";
wrap.appendChild(row);
});
showPreview(title, wrap);
}
Comparing Formats at the Same Quality
PNG, JPEG, and WebP compress very differently. WebP is usually the most space-efficient modern option, but the exact result depends on the picture.
What this code does: Exports the same picture as PNG, JPEG (quality 0.8) and WebP (quality 0.8), prints the three real sizes, and draws them as bars for comparison. A noisy photo-like image makes lossless PNG much larger than the lossy formats. If your browser cannot encode WebP it quietly returns PNG, and the example detects and reports that.
const canvas = makePhoto(400, 300);
const png = canvasToBlob(canvas, "image/png");
const jpeg = canvasToBlob(canvas, "image/jpeg", 0.8);
const webp = canvasToBlob(canvas, "image/webp", 0.8);
console.log("PNG:", (png.size / 1024).toFixed(1), "KB");
console.log("JPEG (0.8):", (jpeg.size / 1024).toFixed(1), "KB");
console.log("WebP (0.8):", (webp.size / 1024).toFixed(1), "KB");
if (webp.type !== "image/webp") {
console.log("This browser cannot encode WebP, so it returned", webp.type);
}
showBars("File size by format", [
{ label: "PNG", size: png.size },
{ label: "JPEG 0.8", size: jpeg.size },
{ label: "WebP 0.8", size: webp.size }
]);
// ===== Helpers below are not part of the lesson =====
function showPreview(label, node) {
setTimeout(() => {
const box = document.createElement("div");
box.className = "demo-preview-box";
box.innerHTML = "<p class='demo-preview-label'>" + label + "</p>";
box.appendChild(node);
document.getElementById("previewContent").appendChild(box);
}, 0);
}
function makePhoto(width, height) {
const c = document.createElement("canvas");
c.width = width;
c.height = height;
const g = c.getContext("2d");
const sky = g.createLinearGradient(0, 0, 0, height);
sky.addColorStop(0, "#1e3a8a");
sky.addColorStop(0.6, "#f59e0b");
sky.addColorStop(1, "#7c2d12");
g.fillStyle = sky;
g.fillRect(0, 0, width, height);
g.fillStyle = "#fde68a";
g.beginPath();
g.arc(width * 0.7, height * 0.35, height * 0.12, 0, Math.PI * 2);
g.fill();
g.fillStyle = "#14532d";
for (let i = 0; i < 6; i++) {
g.beginPath();
g.arc(width * (i / 5), height * 0.9, height * 0.22, 0, Math.PI * 2);
g.fill();
}
let seed = 7;
for (let i = 0; i < (width * height) / 40; i++) {
seed = (seed * 16807) % 2147483647;
const x = seed % width;
seed = (seed * 16807) % 2147483647;
const y = seed % height;
g.fillStyle = "rgba(255, 255, 255, " + ((seed % 100) / 600) + ")";
g.fillRect(x, y, 2, 2);
}
return c;
}
function canvasToBlob(canvas, type, quality) {
const dataUrl = canvas.toDataURL(type, quality);
const mime = dataUrl.slice(5, dataUrl.indexOf(";"));
const binary = atob(dataUrl.split(",")[1]);
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
return new Blob([bytes], { type: mime });
}
function showBars(title, items) {
const wrap = document.createElement("div");
wrap.className = "demo-bars";
wrap.style.cssText = "font:13px sans-serif;color:#0f172a;max-width:420px;";
const max = Math.max(...items.map((item) => item.size));
items.forEach((item) => {
const row = document.createElement("div");
row.className = "demo-bar-row";
row.style.cssText = "display:flex;align-items:center;gap:8px;margin:4px 0;";
row.innerHTML = "<span style='display:inline-block;width:95px'>" + item.label + "</span>"
+ "<div style='flex:1;background:#e2e8f0;border-radius:4px'>"
+ "<div style='height:16px;border-radius:4px;background:#4f46e5;width:" + (item.size / max * 100) + "%'></div></div>"
+ "<span style='display:inline-block;width:75px;text-align:right'>" + (item.size / 1024).toFixed(1) + " KB</span>";
wrap.appendChild(row);
});
showPreview(title, wrap);
}
Calculating the Percentage Saved
Comparing the compressed size against the original uncompressed size gives a clear savings percentage.
What this code does: Calculates the size of the raw pixel data (width x height x 4 bytes), exports the picture as a JPEG at quality 0.7 and works out how much smaller the file is as a percentage. The text output shows the numbers, a two-bar chart compares raw and compressed size, and the compressed picture is displayed.
const canvas = makePhoto(400, 300);
const rawSize = canvas.width * canvas.height * 4; // 4 bytes per pixel (R, G, B, A)
const blob = canvasToBlob(canvas, "image/jpeg", 0.7);
const savedPercent = (100 - (blob.size / rawSize) * 100).toFixed(1);
console.log("Raw pixel data:", (rawSize / 1024).toFixed(1), "KB");
console.log("Compressed size:", (blob.size / 1024).toFixed(1), "KB");
console.log("Reduced by:", savedPercent + "%");
showBars("Raw vs compressed", [
{ label: "Raw pixels", size: rawSize },
{ label: "JPEG 0.7", size: blob.size }
]);
showBlob("The compressed JPEG", blob);
// ===== Helpers below are not part of the lesson =====
function showPreview(label, node) {
setTimeout(() => {
const box = document.createElement("div");
box.className = "demo-preview-box";
box.innerHTML = "<p class='demo-preview-label'>" + label + "</p>";
box.appendChild(node);
document.getElementById("previewContent").appendChild(box);
}, 0);
}
function makePhoto(width, height) {
const c = document.createElement("canvas");
c.width = width;
c.height = height;
const g = c.getContext("2d");
const sky = g.createLinearGradient(0, 0, 0, height);
sky.addColorStop(0, "#1e3a8a");
sky.addColorStop(0.6, "#f59e0b");
sky.addColorStop(1, "#7c2d12");
g.fillStyle = sky;
g.fillRect(0, 0, width, height);
g.fillStyle = "#fde68a";
g.beginPath();
g.arc(width * 0.7, height * 0.35, height * 0.12, 0, Math.PI * 2);
g.fill();
g.fillStyle = "#14532d";
for (let i = 0; i < 6; i++) {
g.beginPath();
g.arc(width * (i / 5), height * 0.9, height * 0.22, 0, Math.PI * 2);
g.fill();
}
let seed = 7;
for (let i = 0; i < (width * height) / 40; i++) {
seed = (seed * 16807) % 2147483647;
const x = seed % width;
seed = (seed * 16807) % 2147483647;
const y = seed % height;
g.fillStyle = "rgba(255, 255, 255, " + ((seed % 100) / 600) + ")";
g.fillRect(x, y, 2, 2);
}
return c;
}
function canvasToBlob(canvas, type, quality) {
const dataUrl = canvas.toDataURL(type, quality);
const mime = dataUrl.slice(5, dataUrl.indexOf(";"));
const binary = atob(dataUrl.split(",")[1]);
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
return new Blob([bytes], { type: mime });
}
function showBlob(label, blob) {
const img = document.createElement("img");
img.className = "demo-export-image";
img.style.cssText = "max-width:100%;border-radius:6px;display:block;";
img.onload = () => URL.revokeObjectURL(img.src);
img.src = URL.createObjectURL(blob);
showPreview(label + " (" + (blob.size / 1024).toFixed(1) + " KB, " + blob.type + ")", img);
}
function showBars(title, items) {
const wrap = document.createElement("div");
wrap.className = "demo-bars";
wrap.style.cssText = "font:13px sans-serif;color:#0f172a;max-width:420px;";
const max = Math.max(...items.map((item) => item.size));
items.forEach((item) => {
const row = document.createElement("div");
row.className = "demo-bar-row";
row.style.cssText = "display:flex;align-items:center;gap:8px;margin:4px 0;";
row.innerHTML = "<span style='display:inline-block;width:95px'>" + item.label + "</span>"
+ "<div style='flex:1;background:#e2e8f0;border-radius:4px'>"
+ "<div style='height:16px;border-radius:4px;background:#4f46e5;width:" + (item.size / max * 100) + "%'></div></div>"
+ "<span style='display:inline-block;width:75px;text-align:right'>" + (item.size / 1024).toFixed(1) + " KB</span>";
wrap.appendChild(row);
});
showPreview(title, wrap);
}
Resizing Often Saves More Than Lowering Quality
Drawing the image onto a smaller canvas before exporting reduces the pixel count itself, which is often a bigger win than only lowering quality.
What this code does: Starts with an 800×600 picture and compares three exports: the original at quality 0.8, the original at a lower quality of 0.4, and a version drawn onto a 400×300 canvas (a quarter of the pixels) at quality 0.8. The bar chart lets you see which change saves most for this picture. The resized image is displayed too. Which option wins depends on the picture, so compare the bars yourself.
const original = makePhoto(800, 600);
// Draw the big image onto a smaller canvas
const resized = document.createElement("canvas");
resized.width = 400;
resized.height = 300;
resized.getContext("2d").drawImage(original, 0, 0, 400, 300);
const fullBlob = canvasToBlob(original, "image/jpeg", 0.8);
const lowQualityBlob = canvasToBlob(original, "image/jpeg", 0.4);
const resizedBlob = canvasToBlob(resized, "image/jpeg", 0.8);
console.log("Original 800x600, quality 0.8:", (fullBlob.size / 1024).toFixed(1), "KB");
console.log("Original 800x600, quality 0.4:", (lowQualityBlob.size / 1024).toFixed(1), "KB");
console.log("Resized 400x300, quality 0.8:", (resizedBlob.size / 1024).toFixed(1), "KB");
showBars("Which change saves more?", [
{ label: "Original 0.8", size: fullBlob.size },
{ label: "Lower quality", size: lowQualityBlob.size },
{ label: "Resized", size: resizedBlob.size }
]);
showBlob("Resized image (400x300)", resizedBlob);
// ===== Helpers below are not part of the lesson =====
function showPreview(label, node) {
setTimeout(() => {
const box = document.createElement("div");
box.className = "demo-preview-box";
box.innerHTML = "<p class='demo-preview-label'>" + label + "</p>";
box.appendChild(node);
document.getElementById("previewContent").appendChild(box);
}, 0);
}
function makePhoto(width, height) {
const c = document.createElement("canvas");
c.width = width;
c.height = height;
const g = c.getContext("2d");
const sky = g.createLinearGradient(0, 0, 0, height);
sky.addColorStop(0, "#1e3a8a");
sky.addColorStop(0.6, "#f59e0b");
sky.addColorStop(1, "#7c2d12");
g.fillStyle = sky;
g.fillRect(0, 0, width, height);
g.fillStyle = "#fde68a";
g.beginPath();
g.arc(width * 0.7, height * 0.35, height * 0.12, 0, Math.PI * 2);
g.fill();
g.fillStyle = "#14532d";
for (let i = 0; i < 6; i++) {
g.beginPath();
g.arc(width * (i / 5), height * 0.9, height * 0.22, 0, Math.PI * 2);
g.fill();
}
let seed = 7;
for (let i = 0; i < (width * height) / 40; i++) {
seed = (seed * 16807) % 2147483647;
const x = seed % width;
seed = (seed * 16807) % 2147483647;
const y = seed % height;
g.fillStyle = "rgba(255, 255, 255, " + ((seed % 100) / 600) + ")";
g.fillRect(x, y, 2, 2);
}
return c;
}
function canvasToBlob(canvas, type, quality) {
const dataUrl = canvas.toDataURL(type, quality);
const mime = dataUrl.slice(5, dataUrl.indexOf(";"));
const binary = atob(dataUrl.split(",")[1]);
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
return new Blob([bytes], { type: mime });
}
function showBlob(label, blob) {
const img = document.createElement("img");
img.className = "demo-export-image";
img.style.cssText = "max-width:100%;border-radius:6px;display:block;";
img.onload = () => URL.revokeObjectURL(img.src);
img.src = URL.createObjectURL(blob);
showPreview(label + " (" + (blob.size / 1024).toFixed(1) + " KB, " + blob.type + ")", img);
}
function showBars(title, items) {
const wrap = document.createElement("div");
wrap.className = "demo-bars";
wrap.style.cssText = "font:13px sans-serif;color:#0f172a;max-width:420px;";
const max = Math.max(...items.map((item) => item.size));
items.forEach((item) => {
const row = document.createElement("div");
row.className = "demo-bar-row";
row.style.cssText = "display:flex;align-items:center;gap:8px;margin:4px 0;";
row.innerHTML = "<span style='display:inline-block;width:95px'>" + item.label + "</span>"
+ "<div style='flex:1;background:#e2e8f0;border-radius:4px'>"
+ "<div style='height:16px;border-radius:4px;background:#4f46e5;width:" + (item.size / max * 100) + "%'></div></div>"
+ "<span style='display:inline-block;width:75px;text-align:right'>" + (item.size / 1024).toFixed(1) + " KB</span>";
wrap.appendChild(row);
});
showPreview(title, wrap);
}
Common Mistake: Treating toBlob() as Synchronous
toBlob() is asynchronous. Reading a variable immediately after calling it (before the callback runs) gives undefined.
What this code does: Shows the classic mistake and the fix. The helper mockToBlob() behaves like the real toBlob(): it returns immediately and calls your callback later. The wrong version reads the result on the very next line and gets undefined. The correct version uses the Blob inside the callback. At the end, runPendingCallbacks() plays the part of the browser finishing the work, and the result finally arrives.
const canvas = makePhoto(400, 300);
let result;
// Wrong: the callback has not run yet when the next line executes
mockToBlob(canvas, (blob) => { result = blob; }, "image/jpeg", 0.8);
console.log("Immediately after calling toBlob:", result);
// Correct: use the Blob inside the callback
mockToBlob(canvas, (blob) => {
console.log("Inside the callback: size", (blob.size / 1024).toFixed(1), "KB");
showBlob("Blob received in the callback", blob);
}, "image/jpeg", 0.8);
// A real browser runs the callbacks after your code finishes; we do it here
console.log("--- the browser finishes encoding ---");
runPendingCallbacks();
console.log("Later, result holds a Blob of type:", result.type);
// ===== Helpers below are not part of the lesson =====
function showPreview(label, node) {
setTimeout(() => {
const box = document.createElement("div");
box.className = "demo-preview-box";
box.innerHTML = "<p class='demo-preview-label'>" + label + "</p>";
box.appendChild(node);
document.getElementById("previewContent").appendChild(box);
}, 0);
}
function makePhoto(width, height) {
const c = document.createElement("canvas");
c.width = width;
c.height = height;
const g = c.getContext("2d");
const sky = g.createLinearGradient(0, 0, 0, height);
sky.addColorStop(0, "#1e3a8a");
sky.addColorStop(0.6, "#f59e0b");
sky.addColorStop(1, "#7c2d12");
g.fillStyle = sky;
g.fillRect(0, 0, width, height);
g.fillStyle = "#fde68a";
g.beginPath();
g.arc(width * 0.7, height * 0.35, height * 0.12, 0, Math.PI * 2);
g.fill();
g.fillStyle = "#14532d";
for (let i = 0; i < 6; i++) {
g.beginPath();
g.arc(width * (i / 5), height * 0.9, height * 0.22, 0, Math.PI * 2);
g.fill();
}
let seed = 7;
for (let i = 0; i < (width * height) / 40; i++) {
seed = (seed * 16807) % 2147483647;
const x = seed % width;
seed = (seed * 16807) % 2147483647;
const y = seed % height;
g.fillStyle = "rgba(255, 255, 255, " + ((seed % 100) / 600) + ")";
g.fillRect(x, y, 2, 2);
}
return c;
}
function canvasToBlob(canvas, type, quality) {
const dataUrl = canvas.toDataURL(type, quality);
const mime = dataUrl.slice(5, dataUrl.indexOf(";"));
const binary = atob(dataUrl.split(",")[1]);
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
return new Blob([bytes], { type: mime });
}
function showBlob(label, blob) {
const img = document.createElement("img");
img.className = "demo-export-image";
img.style.cssText = "max-width:100%;border-radius:6px;display:block;";
img.onload = () => URL.revokeObjectURL(img.src);
img.src = URL.createObjectURL(blob);
showPreview(label + " (" + (blob.size / 1024).toFixed(1) + " KB, " + blob.type + ")", img);
}
const pendingJobs = [];
function mockToBlob(canvas, callback, type, quality) {
pendingJobs.push(() => callback(canvasToBlob(canvas, type, quality)));
}
function runPendingCallbacks() {
pendingJobs.splice(0).forEach((job) => job());
}
PNG vs JPEG vs WebP
| Format | Compression | Transparency | Best For |
|---|---|---|---|
| PNG | Lossless | Yes | Icons, logos, screenshots, text |
| JPEG | Lossy (quality 0–1) | No | Photographs |
| WebP | Lossy or lossless | Yes | Most images, usually the smallest |
Common Mistakes
- Passing a quality value to PNG and expecting the file to shrink
- Re-compressing an already-lossy JPEG repeatedly
- Assuming every browser supports WebP without checking
blob.type - Forgetting to call
URL.revokeObjectURL() - Only lowering quality when resizing would save far more space
- Using the result of
toBlob()outside its callback
Best Practices
- Use JPEG or WebP for photographs; PNG for graphics that need transparency
- Resize large images to their display size before compressing
- Test quality values in the 0.6–0.85 range
- Always treat
toBlob()as asynchronous (toDataURL()is synchronous but produces larger text) - Revoke object URLs when they are no longer needed
Quick Summary
- Draw to a canvas, then export with
toBlob()ortoDataURL() - PNG is lossless and ignores quality; JPEG and WebP use a 0–1 quality value
- WebP is generally the smallest modern option
- Resizing dimensions usually saves more space than lowering quality alone
toBlob()is always asynchronous, so use the callback
Try It Yourself
What this code does: Combines raw size calculation, JPEG export and a savings percentage on a larger 1200×800 picture. Change the three settings at the top (width, height and quality) and run it again to see how the file size and the savings react. The output shows the numbers, a bar chart (raw size against JPEG size) and the exported picture.
const width = 1200;
const height = 800;
const quality = 0.75;
const canvas = makePhoto(width, height);
const rawSize = canvas.width * canvas.height * 4;
console.log("Original size:", (rawSize / 1024 / 1024).toFixed(2), "MB");
const blob = canvasToBlob(canvas, "image/jpeg", quality);
const saved = (100 - (blob.size / rawSize) * 100).toFixed(1);
console.log("JPEG (" + quality + "):", (blob.size / 1024).toFixed(1), "KB, saved", saved + "%");
showBars("Raw vs JPEG", [
{ label: "Raw pixels", size: rawSize },
{ label: "JPEG " + quality, size: blob.size }
]);
showBlob("Result (" + width + "x" + height + ")", blob);
// ===== Helpers below are not part of the lesson =====
function showPreview(label, node) {
setTimeout(() => {
const box = document.createElement("div");
box.className = "demo-preview-box";
box.innerHTML = "<p class='demo-preview-label'>" + label + "</p>";
box.appendChild(node);
document.getElementById("previewContent").appendChild(box);
}, 0);
}
function makePhoto(width, height) {
const c = document.createElement("canvas");
c.width = width;
c.height = height;
const g = c.getContext("2d");
const sky = g.createLinearGradient(0, 0, 0, height);
sky.addColorStop(0, "#1e3a8a");
sky.addColorStop(0.6, "#f59e0b");
sky.addColorStop(1, "#7c2d12");
g.fillStyle = sky;
g.fillRect(0, 0, width, height);
g.fillStyle = "#fde68a";
g.beginPath();
g.arc(width * 0.7, height * 0.35, height * 0.12, 0, Math.PI * 2);
g.fill();
g.fillStyle = "#14532d";
for (let i = 0; i < 6; i++) {
g.beginPath();
g.arc(width * (i / 5), height * 0.9, height * 0.22, 0, Math.PI * 2);
g.fill();
}
let seed = 7;
for (let i = 0; i < (width * height) / 40; i++) {
seed = (seed * 16807) % 2147483647;
const x = seed % width;
seed = (seed * 16807) % 2147483647;
const y = seed % height;
g.fillStyle = "rgba(255, 255, 255, " + ((seed % 100) / 600) + ")";
g.fillRect(x, y, 2, 2);
}
return c;
}
function canvasToBlob(canvas, type, quality) {
const dataUrl = canvas.toDataURL(type, quality);
const mime = dataUrl.slice(5, dataUrl.indexOf(";"));
const binary = atob(dataUrl.split(",")[1]);
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
return new Blob([bytes], { type: mime });
}
function showBlob(label, blob) {
const img = document.createElement("img");
img.className = "demo-export-image";
img.style.cssText = "max-width:100%;border-radius:6px;display:block;";
img.onload = () => URL.revokeObjectURL(img.src);
img.src = URL.createObjectURL(blob);
showPreview(label + " (" + (blob.size / 1024).toFixed(1) + " KB, " + blob.type + ")", img);
}
function showBars(title, items) {
const wrap = document.createElement("div");
wrap.className = "demo-bars";
wrap.style.cssText = "font:13px sans-serif;color:#0f172a;max-width:420px;";
const max = Math.max(...items.map((item) => item.size));
items.forEach((item) => {
const row = document.createElement("div");
row.className = "demo-bar-row";
row.style.cssText = "display:flex;align-items:center;gap:8px;margin:4px 0;";
row.innerHTML = "<span style='display:inline-block;width:95px'>" + item.label + "</span>"
+ "<div style='flex:1;background:#e2e8f0;border-radius:4px'>"
+ "<div style='height:16px;border-radius:4px;background:#4f46e5;width:" + (item.size / max * 100) + "%'></div></div>"
+ "<span style='display:inline-block;width:75px;text-align:right'>" + (item.size / 1024).toFixed(1) + " KB</span>";
wrap.appendChild(row);
});
showPreview(title, wrap);
}
Tip: Resize the canvas before you compress it, pick JPEG or WebP for photographs and PNG for graphics that need transparency, and always treat the export as asynchronous.
🧪 Test Your JavaScript Code
Edit the code on the left and click “Run Code” to see the console output on the right.
Click “Run Code” to see the output here.
Code Explanation
The complete example stays visible. Use Previous and Next to move through its logical blocks.
Select Next to begin exploring the code.