前端零依赖:5KB 单页图片压缩器

需要快速压缩几张图片,又懒得开 PS / Affinity?用下面这段小程序,一行 <iframe> 就能嵌进你的站。

为什么写?

  • 做博客封面,要把 JPG 从 5 MB 压到 200 KB。
  • 在线网站对上传图片的尺寸/格式要求(如 <1MB 的 JPG)
  • 给产品文档贴图,想批量缩小分辨率。
  • 不想上传到第三方服务,担心隐私+连通性。

于是就有了这款 纯前端、本地运行、0 后端依赖 的图片压缩器。

在线体验

🖼️ Image Compressor

功能一览

  • 拖拽 / 点击 批量上传 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>

祝压缩愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Back to Top