需要快速压缩几张图片,又懒得开 PS / Affinity?用下面这段小程序,一行 <iframe>
就能嵌进你的站。
为什么写?
- 做博客封面,要把 JPG 从 5 MB 压到 200 KB。
- 在线网站对上传图片的尺寸/格式要求(如 <1MB 的 JPG)
- 给产品文档贴图,想批量缩小分辨率。
- 不想上传到第三方服务,担心隐私+连通性。
于是就有了这款 纯前端、本地运行、0 后端依赖 的图片压缩器。
在线体验
功能一览
- 拖拽 / 点击 批量上传
JPG/PNG/WEBP
。 - 质量、分辨率 滑杆实时预览,显示压缩后尺寸。
- 输出 JPEG / WebP,支持单张或一键全部下载。
- 删除文件 & 一键重置,浏览器全部本地处理,安全省心。
- 中 / 英双语,自动按
navigator.language
识别,可手动切换。
连通性小贴士
- 所有脚本来自 UNPKG / jsDelivr;如在中国大陆访问受阻,可改成国内镜像,或将脚本打包到本地。
- Tailwind 也可
npm i
后自行构建,去掉 CDN 依赖。
源码
全部只有 一份 5KB 的 index.html
(已压缩后体积),没有打包步骤,随时可二次开发。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>🖼️ Image Compressor</title>
<!-- Tailwind via CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- React 18 UMD -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!-- Babel (runtime JSX transform) -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
<body class="bg-gray-50 min-h-screen flex flex-col items-center py-8">
<div id="root"></div>
<!-- Main App -------------------------------------------------------------- -->
<script type="text/babel">
const { useState, useEffect, useRef } = React;
const fmtKB = (bytes) => `${(bytes / 1024).toFixed(1)} KB`;
function ImageCompressor() {
const [files, setFiles] = useState([]); // [{file, url, compressedUrl, compressedSize, width, height}]
const [quality, setQuality] = useState(80); // 10–100 (JPEG/WebP quality)
const [scale, setScale] = useState(100); // 10–100 (%)
const [format, setFormat] = useState("image/jpeg"); // "image/jpeg" | "image/webp"
// Drag‑and‑drop state
const [dragging, setDragging] = useState(false);
const dropRef = useRef(null);
/* ------------------------------------------------------------------ */
// File intake (input & DnD)
const handleFileList = (fileList) => {
const imgs = Array.from(fileList).filter((f) => f.type.startsWith("image/"));
if (!imgs.length) return;
const mapped = imgs.map((f) => ({
file: f,
name: f.name,
url: URL.createObjectURL(f),
compressedUrl: "",
compressedSize: 0,
width: 0,
height: 0,
}));
setFiles(mapped);
};
const onFileInput = (e) => handleFileList(e.target.files);
const onDrop = (e) => {
e.preventDefault();
setDragging(false);
handleFileList(e.dataTransfer.files);
};
const onDragOver = (e) => {
e.preventDefault();
setDragging(true);
};
const onDragLeave = (e) => {
e.preventDefault();
setDragging(false);
};
/* ------------------------------------------------------------------ */
// Re‑compress whenever files or settings change
useEffect(() => {
if (!files.length) return;
files.forEach((item, idx) => {
const img = new Image();
img.src = item.url;
img.onload = () => {
const canvas = document.createElement("canvas");
const targetW = Math.round((img.width * scale) / 100);
const targetH = Math.round((img.height * scale) / 100);
canvas.width = targetW;
canvas.height = targetH;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, targetW, targetH);
canvas.toBlob(
(blob) => {
setFiles((prev) => {
const copy = [...prev];
// Revoke previous blob URL if exists
if (copy[idx].compressedUrl) URL.revokeObjectURL(copy[idx].compressedUrl);
copy[idx] = {
...copy[idx],
compressedUrl: URL.createObjectURL(blob),
compressedSize: blob.size,
width: targetW,
height: targetH,
};
return copy;
});
},
format,
quality / 100
);
};
});
// Cleanup blob URLs on unmount
return () => {
files.forEach((f) => f.compressedUrl && URL.revokeObjectURL(f.compressedUrl));
};
}, [files.length, quality, scale, format]);
/* ------------------------------------------------------------------ */
const downloadAll = () => {
if (!files.length) return;
files.forEach((f) => {
if (!f.compressedUrl) return;
const a = document.createElement("a");
a.href = f.compressedUrl;
const ext = format === "image/webp" ? ".webp" : ".jpg";
a.download = `${f.name.replace(/\.[^/.]+$/, "")}_compressed${ext}`;
a.click();
});
};
/* ------------------------------------------------------------------ */
return (
<div className="w-full max-w-4xl">
<h1 className="text-3xl font-bold mb-6 text-center">🖼️ Image Compressor</h1>
{/* Upload / DnD area */}
<div
ref={dropRef}
onDrop={onDrop}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onClick={() => document.getElementById("fileInput").click()}
className={`border-4 border-dashed rounded-xl p-8 text-center transition-colors cursor-pointer ${dragging ? "border-blue-500 bg-blue-50" : "border-gray-300 bg-white"}`}
>
<p className="text-lg font-medium mb-2">点击或拖拽图片到此</p>
<p className="text-sm text-gray-500">支持批量 · JPG / PNG / WEBP</p>
<input
id="fileInput"
type="file"
accept="image/*"
multiple
hidden
onChange={onFileInput}
/>
</div>
{/* Settings + Preview */}
{files.length > 0 && (
<div className="mt-8 space-y-6">
{/* Global settings */}
<div className="grid md:grid-cols-3 gap-4">
{/* Quality */}
<div>
<label className="text-sm font-medium">质量: {quality}%</label>
<input
type="range"
min="10"
max="100"
value={quality}
onChange={(e) => setQuality(parseInt(e.target.value))}
className="w-full"
/>
</div>
{/* Scale */}
<div>
<label className="text-sm font-medium">分辨率: {scale}%</label>
<input
type="range"
min="10"
max="100"
value={scale}
onChange={(e) => setScale(parseInt(e.target.value))}
className="w-full"
/>
</div>
{/* Format */}
<div>
<label className="text-sm font-medium">格式:</label>
<select
value={format}
onChange={(e) => setFormat(e.target.value)}
className="ml-2 border rounded p-1"
>
<option value="image/jpeg">JPEG</option>
<option value="image/webp">WEBP</option>
</select>
</div>
</div>
<button
onClick={downloadAll}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg shadow"
>
下载全部
</button>
{/* Image previews */}
<div className="grid sm:grid-cols-2 gap-6">
{files.map((f, i) => (
<div
key={i}
className="bg-white p-4 rounded-xl shadow flex flex-col items-center"
>
<img
src={f.compressedUrl || f.url}
alt="preview"
className="max-h-60 object-contain rounded"
/>
<p className="mt-2 text-xs text-gray-600 text-center break-all">
{f.name}
</p>
{f.compressedUrl && (
<p className="text-xs text-gray-500">
{f.width}×{f.height} · {fmtKB(f.compressedSize)}
</p>
)}
{f.compressedUrl && (
<a
href={f.compressedUrl}
download={`${f.name.replace(/\.[^/.]+$/, "")}_compressed${
format === "image/webp" ? ".webp" : ".jpg"
}`}
className="mt-2 text-blue-600 underline"
>
单独下载
</a>
)}
</div>
))}
</div>
</div>
)}
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<ImageCompressor />);
</script>
</body>
</html>
祝压缩愉快!