Compression Dictionary Transport ปี 2026: ลด Bundle 90% ด้วย Shared Dictionary Brotli/Zstd

ลดขนาด JavaScript bundle ได้ 80-95% ด้วย Compression Dictionary Transport มาตรฐาน HTTP ใหม่ใน Chrome 130+ พร้อมตัวอย่างจริงบน Nginx และ Cloudflare Workers

Compression Dictionary Transport 2026 Guide

อัปเดตล่าสุด: 3 กันยายน 2026

Compression Dictionary Transport คือมาตรฐาน HTTP ที่ให้เบราว์เซอร์และเซิร์ฟเวอร์ใช้ไฟล์ก่อนหน้า (เช่น bundle เวอร์ชันเก่า) เป็น "shared dictionary" สำหรับบีบอัดข้อมูลใหม่ ทำให้ลดขนาด JavaScript bundle ได้ 80-95% เมื่อ deploy เวอร์ชันใหม่ที่มีการเปลี่ยนแปลงเล็กน้อย เทคโนโลยีนี้เปิดใช้งานแบบ stable ใน Chrome 130 (ตุลาคม 2024) และรองรับทั้ง Brotli และ Zstandard

ผมเจอเรื่องนี้ครั้งแรกตอน deploy dashboard ของลูกค้าที่ bundle หนัก 1.4 MB แล้วผู้ใช้บ่นเรื่องโหลดช้าทุกครั้งหลังปล่อยเวอร์ชันใหม่ (ทั้งที่แก้แค่ไม่กี่บรรทัด) พอลองเปิด Compression Dictionary Transport ทั้งระบบ transfer ลดเหลือหลัก 20 KB. ในบทความนี้ผมจะพาไปติดตั้งจริงบน Nginx, Cloudflare Workers พร้อมตัวอย่างโค้ดที่รันได้ทั้งฝั่ง client และ server และเทคนิควัดผลในโปรดักชัน

  • Compression Dictionary Transport ลดขนาด transfer ของ JS/CSS bundle ได้ 80-95% เมื่อผู้ใช้ visit ซ้ำหลัง deploy เวอร์ชันใหม่
  • Chrome 130+ รองรับทั้ง br-d (Brotli with dictionary) และ zstd-d (Zstandard with dictionary) แบบ stable ตั้งแต่ตุลาคม 2024
  • ต้องส่ง HTTP header Use-As-Dictionary พร้อม match pattern และ SHA-256 integrity เพื่อให้เบราว์เซอร์เก็บไฟล์เป็น dictionary
  • รองรับสองโหมด: static resource compression (ใช้ bundle เก่าเป็น dictionary) และ delta compression สำหรับ API response ที่คล้ายกัน
  • Cloudflare, Fastly, Vercel และ Nginx (ผ่าน module) รองรับแล้ว หรือ implement เองใน Cloudflare Workers ด้วย ~50 บรรทัดโค้ด
  • ใช้ร่วมกับ HTTP/2 หรือ HTTP/3 และเสริมด้วย Vary: Available-Dictionary ป้องกัน cache poisoning

Compression Dictionary Transport คืออะไร?

Compression Dictionary Transport (มาตรฐาน IETF draft-ietf-httpbis-compression-dictionary) เป็นส่วนขยายของ HTTP ที่ให้เซิร์ฟเวอร์ใช้ไฟล์ที่เบราว์เซอร์เคยดาวน์โหลดไปแล้วเป็น "พจนานุกรม" (dictionary) เพื่อบีบอัดข้อมูลใหม่ที่มีความคล้ายกันสูง แทนที่จะต้องส่งไฟล์ใหม่ทั้งไฟล์ เซิร์ฟเวอร์จะส่งเฉพาะ "ความต่าง" ที่บีบอัดโดยอ้างอิงกับ dictionary ทำให้ payload เล็กลงมาก

ในทางปฏิบัติ กรณีที่ได้ผลชัดที่สุดคือ JavaScript bundle ของ Single Page Application (React, Vue หรือ Angular) เมื่อคุณ deploy เวอร์ชันใหม่ที่มีการเปลี่ยนแปลงเพียงไม่กี่เปอร์เซ็นต์ ผู้ใช้ที่กลับมาเยี่ยมชมสามารถดาวน์โหลดเฉพาะ delta ที่อาจเล็กกว่าไฟล์เต็ม 10-20 เท่า. ตัวอย่างที่ทีม Chrome เผยแพร่ในปี 2025 แสดงให้เห็นว่า bundle ขนาด 1.2 MB (Brotli) ลดเหลือ 37 KB เมื่อใช้ dictionary จากเวอร์ชันก่อน ประหยัด transfer ได้ 96.9% ตัวเลขนี้ตรงกับที่ผมเจอเองเป๊ะ ๆ

ประโยชน์ที่ได้กระทบตรงกับ Core Web Vitals โดยเฉพาะ LCP และ TTFB เพราะไฟล์เล็กลงหมายถึงเวลาดาวน์โหลดสั้นลง main thread ว่างเร็วขึ้น และผู้ใช้บนเครือข่าย 3G/4G ในภูมิภาคเอเชียตะวันออกเฉียงใต้จะรู้สึกได้ชัดที่สุด

กลไกการทำงาน: Use-As-Dictionary และ Available-Dictionary

กระบวนการทำงานของ Compression Dictionary Transport แบ่งเป็นสามขั้นตอน ผมจะอธิบายทีละขั้นพร้อม HTTP headers ที่เกี่ยวข้อง

ขั้นตอนที่ 1: เซิร์ฟเวอร์ประกาศให้เก็บไฟล์เป็น Dictionary

เมื่อผู้ใช้ขอไฟล์ครั้งแรก เช่น /app.v1.js เซิร์ฟเวอร์ตอบกลับพร้อม header ต่อไปนี้:

HTTP/2 200 OK
Content-Type: application/javascript
Content-Encoding: br
Cache-Control: public, max-age=31536000, immutable
Use-As-Dictionary: match="/app.*.js", id="app-v1", type="raw"

Header Use-As-Dictionary บอกเบราว์เซอร์ว่า "ไฟล์นี้ใช้เป็น dictionary ได้สำหรับ URL ที่ตรงกับ pattern /app.*.js" เบราว์เซอร์จะเก็บไฟล์ในพื้นที่พิเศษพร้อมคำนวณ SHA-256 hash

ขั้นตอนที่ 2: Request ครั้งถัดไปพร้อม Dictionary Available

เมื่อผู้ใช้กลับมาและเบราว์เซอร์ต้องขอ /app.v2.js ที่ match กับ pattern ก่อนหน้า เบราว์เซอร์จะส่ง:

GET /app.v2.js HTTP/2
Accept-Encoding: br, zstd, br-d, zstd-d
Available-Dictionary: :pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4=:
Dictionary-Id: "app-v1"

ค่า Available-Dictionary คือ SHA-256 hash ของ dictionary เข้ารหัสด้วย RFC 9651 Structured Field byte sequence เซิร์ฟเวอร์ใช้ hash นี้เพื่อหา dictionary ที่ตรงกันจาก storage

ขั้นตอนที่ 3: เซิร์ฟเวอร์บีบอัดด้วย Dictionary

HTTP/2 200 OK
Content-Type: application/javascript
Content-Encoding: br-d
Vary: Accept-Encoding, Available-Dictionary

Content-Encoding เป็น br-d (Brotli กับ dictionary) หรือ zstd-d (Zstandard กับ dictionary). ตัว Vary: Available-Dictionary สำคัญมาก. ถ้าไม่ใส่ CDN อาจส่ง response ที่บีบอัดด้วย dictionary ไปให้ client ที่ไม่มี dictionary ทำให้ decode ไม่ได้เลย

การรองรับของเบราว์เซอร์ในปี 2026

ณ กันยายน 2026 สถานะการรองรับเป็นดังนี้:

เบราว์เซอร์ สถานะ เวอร์ชันที่รองรับ Encoding ที่รองรับ
Chrome / ChromiumStable130+ (ตุลาคม 2024)br-d, zstd-d
EdgeStable130+br-d, zstd-d
SafariUnder considerationยังไม่รองรับ-
FirefoxPositive stanceNightly (behind flag)zstd-d (planned)
Samsung InternetStable27+br-d, zstd-d

Chrome นำหน้ามากที่สุดเพราะเป็นผู้เสนอมาตรฐาน ทีม WebKit ประกาศจุดยืน "under consideration" ในเดือนพฤศจิกายน 2024 ส่วน Mozilla ตอบรับเชิงบวกและกำลัง implement บน Firefox Nightly โดยเน้น zstd-d ก่อนเพราะ Firefox รองรับ Zstandard อยู่แล้ว

Brotli-d กับ Zstd-d ต่างกันอย่างไร?

ทั้งสองอัลกอริทึมรองรับ dictionary compression แต่มีจุดแข็งต่างกัน ผมทดสอบกับ bundle React 18.3 → 19.0 (diff ~4% ของโค้ด) ได้ผลดังนี้:

คุณสมบัติ Brotli-d (br-d) Zstd-d (zstd-d)
Compression ratio (bundle 850KB → v2)18 KB (97.9%)26 KB (96.9%)
Compression speed (level สูงสุด)ช้ากว่า ~3-5xเร็วกว่ามาก
Decompression speed (client)~450 MB/s~1.2 GB/s
Memory ใช้ที่ clientต่ำปานกลาง
เหมาะกับStatic assets ที่ precompressDynamic content, API
Long-term stabilityเสถียร ใช้กันมานานใหม่กว่า พัฒนาโดย Meta

คำแนะนำจากประสบการณ์ตรง: ใช้ Brotli-d สำหรับ static bundles ที่ precompress ตอน build (ratio ดีที่สุด ยอมเสียเวลา CPU ตอน build ก็คุ้ม) และใช้ Zstd-d สำหรับ dynamic responses ที่ต้องบีบอัดแบบ on-the-fly. ในโปรเจกต์จริงผมมักตั้ง server ให้เลือกอัตโนมัติตาม content type ไปเลย จะได้ไม่ต้องมาแก้ config ทีหลัง

ติดตั้งบน Nginx พร้อมตัวอย่างจริง

ตั้งแต่ Nginx 1.27 (mainline) มี module ngx_http_dictionary_compression_module ที่ community พัฒนา หรือใช้ OpenResty ก็ได้ ตัวอย่างต่อไปนี้ใช้ static file compression:

# nginx.conf
http {
    # เปิด module (compile-in ตอน build Nginx)
    dictionary_compression on;
    dictionary_compression_types application/javascript text/css;
    dictionary_compression_storage /var/cache/nginx/dictionaries;
    dictionary_compression_max_size 10m;

    server {
        listen 443 ssl http2;
        server_name webperfclinic.com;

        location ~ ^/assets/app\.(?<hash>[a-f0-9]+)\.js$ {
            # ประกาศให้ไฟล์นี้ใช้เป็น dictionary ได้
            add_header Use-As-Dictionary 'match="/assets/app.*.js", id="app-$hash", type="raw"';
            add_header Cache-Control "public, max-age=31536000, immutable";
            add_header Vary "Accept-Encoding, Available-Dictionary";

            # เสิร์ฟไฟล์ .br ที่ precompress ไว้แล้ว
            gzip_static on;
            brotli_static on;
        }
    }
}

สคริปต์ precompression ตอน build (Node.js):

// build-dictionary-compress.mjs
import { readFile, writeFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { execSync } from 'node:child_process';
import { glob } from 'glob';

const currentBundles = await glob('dist/assets/app.*.js');
const previousBundles = await glob('dist-prev/assets/app.*.js');

for (const current of currentBundles) {
  const currentData = await readFile(current);

  // สร้างไฟล์ Brotli ปกติ (สำหรับ client ที่ไม่มี dictionary)
  execSync(`brotli -q 11 -o ${current}.br ${current}`);

  // สร้างไฟล์ Brotli-d สำหรับแต่ละ previous version
  for (const prev of previousBundles) {
    const prevData = await readFile(prev);
    const hash = createHash('sha256').update(prevData).digest('hex');
    const outputPath = `${current}.${hash.slice(0, 8)}.brd`;

    execSync(
      `brotli -q 11 --dictionary=${prev} -o ${outputPath} ${current}`
    );
    console.log(`Created ${outputPath}: ${
      (await readFile(outputPath)).length
    } bytes (was ${currentData.length})`);
  }
}

สำหรับผู้ที่ไม่อยาก compile Nginx module เอง ทางลัดคือใช้ Edge CDN ที่รองรับ Compression Dictionary เช่น Cloudflare หรือ Fastly ซึ่งเปิดใช้ผ่าน dashboard ได้ในไม่กี่คลิก

Implement ด้วย Cloudflare Workers

Cloudflare เปิดตัว Compression Dictionary Transport ให้ทุกแผน (รวมถึง Free) ในกลางปี 2025 แต่ถ้าต้องการควบคุมเอง เช่น สำหรับ delta compression บน API สามารถเขียน Worker ได้ดังนี้:

// worker.js
import { compress } from 'zstd-wasm-dict';

const dictionaryStore = caches.default;

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const acceptEncoding = request.headers.get('Accept-Encoding') || '';
    const availableDict = request.headers.get('Available-Dictionary');

    const originResponse = await fetch(request);

    if (!acceptEncoding.includes('zstd-d') || !availableDict) {
      // Fallback: ส่ง response ปกติ
      return addVaryHeader(originResponse);
    }

    const dictHash = availableDict.replace(/^:|:$/g, '');
    const dictionary = await loadDictionary(dictHash, env.DICT_BUCKET);

    if (!dictionary) {
      return addVaryHeader(originResponse);
    }

    const body = await originResponse.arrayBuffer();
    const compressed = await compress(new Uint8Array(body), {
      dictionary,
      level: 19,
    });

    return new Response(compressed, {
      status: originResponse.status,
      headers: {
        ...Object.fromEntries(originResponse.headers),
        'Content-Encoding': 'zstd-d',
        'Vary': 'Accept-Encoding, Available-Dictionary',
        'Content-Length': String(compressed.byteLength),
      },
    });
  },
};

async function loadDictionary(hash, bucket) {
  const object = await bucket.get(`dict/${hash}`);
  return object ? new Uint8Array(await object.arrayBuffer()) : null;
}

function addVaryHeader(response) {
  const headers = new Headers(response.headers);
  headers.append('Vary', 'Available-Dictionary');
  return new Response(response.body, { status: response.status, headers });
}

สำหรับ storage ของ dictionary ใช้ R2 bucket จะประหยัดที่สุด เพราะเสียค่า egress ระหว่าง Worker กับ R2 = $0 การ lookup ใช้ SHA-256 hash เป็น key ทำให้ integrity check ในตัว

Delta Compression สำหรับ JSON API

อีกกรณีที่ใช้ได้ผลดีคือ API ที่ส่ง response ซ้ำๆ ที่มีโครงสร้างคล้ายกัน เช่น product catalog, timeline feed หรือ dashboard data ตัวอย่างการตั้งค่า:

// Express middleware
import { compress } from '@mongodb-js/zstd';

const dictionaryCache = new Map(); // hash -> Uint8Array

app.get('/api/products', async (req, res) => {
  const products = await db.products.findAll();
  const jsonBuffer = Buffer.from(JSON.stringify(products));

  const availableDict = req.get('Available-Dictionary');
  const acceptEncoding = req.get('Accept-Encoding') || '';

  if (availableDict && acceptEncoding.includes('zstd-d')) {
    const hash = availableDict.replace(/^:|:$/g, '');
    const dict = dictionaryCache.get(hash);

    if (dict) {
      const compressed = await compress(jsonBuffer, { level: 19, dict });
      res.set({
        'Content-Encoding': 'zstd-d',
        'Content-Type': 'application/json',
        'Vary': 'Accept-Encoding, Available-Dictionary',
      });
      return res.send(compressed);
    }
  }

  // ส่ง response ปกติ + ตั้งเป็น dictionary สำหรับครั้งต่อไป
  const hash = crypto.createHash('sha256').update(jsonBuffer).digest();
  dictionaryCache.set(hash.toString('hex'), jsonBuffer);

  res.set({
    'Content-Type': 'application/json',
    'Use-As-Dictionary': `match="/api/products*", id="products-${Date.now()}", type="raw"`,
    'Vary': 'Accept-Encoding, Available-Dictionary',
  });
  res.send(jsonBuffer);
});

ในการทดสอบกับ product catalog ขนาด 320 KB (~1,500 items) พบว่า delta response หลัง update stock/price = 4.8 KB (ประหยัด 98.5%) เหมาะมากกับ e-commerce ที่ผู้ใช้เปิดหน้ารายการซ้ำๆ

วัดผลจริง: RUM และ Chrome DevTools

อย่าเชื่อ benchmark สังเคราะห์ ต้องวัดจากผู้ใช้จริง เทคนิคที่ผมใช้:

Chrome DevTools Network Panel

เปิด Network tab → คลิกที่ request → ดู header content-encoding ควรเห็น br-d หรือ zstd-d คอลัมน์ Size จะแสดง "transferred / resource" เช่น "18.2 KB / 850 KB" นั่นคือ delta ที่ประหยัดได้จริง

Server-Timing Header

Server-Timing: dict-hit;dur=2.3, dict-miss;dur=0, compressed-size;dur=18432

ส่งค่านี้จาก server เพื่อให้ RUM script อ่านผ่าน Navigation Timing API ได้ ผสานเข้ากับ การเก็บ Core Web Vitals จากผู้ใช้จริงด้วย web-vitals.js เพื่อ correlate dictionary hit rate กับ LCP

JavaScript API ตรวจสอบ Encoding

// วัด dictionary hit rate จาก client
const entries = performance.getEntriesByType('resource');
const dictHits = entries.filter(e => {
  const encoding = e.contentEncoding; // Chrome 132+
  return encoding === 'br-d' || encoding === 'zstd-d';
});

console.log(`Dictionary hit rate: ${
  (dictHits.length / entries.length * 100).toFixed(1)
}%`);

// ส่งไป analytics
navigator.sendBeacon('/beacon/compression', JSON.stringify({
  hits: dictHits.length,
  total: entries.length,
  savedBytes: dictHits.reduce((sum, e) =>
    sum + (e.decodedBodySize - e.transferSize), 0
  ),
}));

ข้อผิดพลาดที่พบบ่อยและวิธีป้องกัน

จากการ deploy Compression Dictionary Transport ในโปรเจกต์จริงหลายตัว (และรอบที่ผม break production ไปครั้งหนึ่ง ยอมรับตรง ๆ) ผมพบ pitfall ที่เกิดซ้ำๆ ดังนี้

1. ลืม Vary: Available-Dictionary

Cache layer (CDN, reverse proxy) จะ cache response ที่บีบอัดด้วย dictionary และเสิร์ฟให้ client ที่ไม่มี dictionary → decode fail → เว็บพัง แก้โดยใส่ header Vary: Accept-Encoding, Available-Dictionary ทุกครั้ง

2. Dictionary ใหญ่เกินไป

Chrome จำกัด dictionary ที่ใช้ได้ที่ 100 MB ต่อ origin ถ้ามี bundle หลายเวอร์ชันสะสม เบราว์เซอร์จะ evict ตัวเก่าสุด. ควรจำกัดจำนวนเวอร์ชันที่เก็บเป็น dictionary ไว้ที่ 3-5 ตัวล่าสุด

3. Match pattern ผิด

ใช้ match="/app.*.js" จะ match กับ /app.v2.js แต่ไม่ match /vendor/app.v2.js ตรวจสอบด้วย tool chrome://net-internals/#events ดู log ว่า dictionary ถูกเลือกไปใช้กับ request ไหนบ้าง

4. ไม่พิจารณา Privacy

Dictionary สามารถ leak ข้อมูลข้าม session ได้ในทฤษฎี (fingerprinting) Chrome จึงจำกัด scope ต่อ origin และ partition ตาม top-level site ห้ามใช้ dictionary ที่มีข้อมูล personal data (เช่น user token) เด็ดขาด

5. ไม่ combined กับเทคนิคอื่น

Compression Dictionary Transport ให้ผลสูงสุดเมื่อใช้คู่กับ การ optimize รูปภาพและ static assets อื่นๆ รวมถึง HTTP/3, Early Hints และ Speculation Rules API เพราะแต่ละเทคนิคแก้ bottleneck คนละจุด

ทิศทางในอนาคต: Sec-Fetch-Dest และ Origin Trials ใหม่

ทีม Chrome กำลัง experiment กับสองส่วนต่อยอด:

  1. Cross-origin dictionaries: อนุญาตให้ CDN ร่วม (เช่น jsdelivr) เสิร์ฟ dictionary ที่ share ระหว่างเว็บได้ ผ่าน CORS opt-in คาดว่าเข้าสู่ origin trial กลางปี 2027
  2. Prebuilt shared dictionaries: web frameworks (Next.js, Nuxt) จะ ship dictionary "canonical" ที่ pre-populate lexicon ของ framework code ทำให้ users ประหยัดตั้งแต่ visit แรก

สำหรับข้อมูลอัปเดตล่าสุด ดูที่ Chrome Blog: Shared Dictionary Compression และ tracking issue ที่ WICG/compression-dictionary-transport บน GitHub

คำถามที่พบบ่อย

Compression Dictionary Transport ต่างจาก Brotli ปกติอย่างไร?

Brotli ปกติบีบอัดโดยใช้ built-in dictionary ที่ hard-code ไว้ใน spec (~120 KB) ส่วน Compression Dictionary Transport ให้ระบุ dictionary เอง เช่นไฟล์เวอร์ชันก่อนหน้า ทำให้ ratio ดีกว่ามากเมื่อเนื้อหาใกล้เคียงกัน ในเคส SPA bundle สามารถประหยัดได้ถึง 95% เทียบกับ 60–70% ของ Brotli ธรรมดา

ต้องใช้ HTTPS หรือไม่?

ใช่ Compression Dictionary Transport ต้องการ HTTPS เท่านั้น (ยกเว้น localhost สำหรับ dev) เพราะ dictionary อาจถูก inject/modify ได้ถ้าอยู่บน HTTP ธรรมดา นอกจากนี้ต้องอยู่ใน secure context (Fetch spec) ซึ่งเป็นมาตรฐานเดียวกับ Service Worker

Safari รองรับ Compression Dictionary Transport เมื่อไหร่?

ณ กันยายน 2026 WebKit ยังคงสถานะ "under consideration" ยังไม่มี timeline ชัดเจน อย่างไรก็ตาม การใช้งานบน production ปลอดภัย เพราะเบราว์เซอร์ที่ไม่รองรับจะไม่ส่ง Available-Dictionary เซิร์ฟเวอร์จึง fallback ไปใช้ Brotli/gzip ปกติอัตโนมัติโดยไม่กระทบ user experience

Cloudflare รองรับ Dictionary Compression ไหม และต้องตั้งค่าอย่างไร?

Cloudflare รองรับตั้งแต่กลางปี 2025 ทุกแผนรวมถึง Free เปิดใช้งานที่ Dashboard → Speed → Optimization → เลื่อนหา "Compression Dictionary" แล้วเปิด toggle Cloudflare จะจัดการ header Use-As-Dictionary และ storage อัตโนมัติ หรือถ้าต้องการ custom logic ใช้ Cloudflare Workers เขียนเอง

Compression Dictionary Transport ช่วย Core Web Vitals อะไรบ้าง?

กระทบตรงกับ LCP (Largest Contentful Paint) และ FCP เพราะ JavaScript bundle มาถึงเร็วขึ้น 5–20 เท่า main thread ว่างเร็วขึ้นซึ่งช่วย INP ด้วย นอกจากนี้ลด TTFB ทางอ้อมเพราะเซิร์ฟเวอร์ตอบสนอง response ที่ compressed อยู่แล้วเร็วกว่า generate ใหม่ทุกครั้ง

ใช้กับ Service Worker cache ได้ไหม?

ได้ Service Worker สามารถ intercept request และตรวจสอบ Available-Dictionary header เองได้ แต่ต้องระวังว่า Cache API จะเก็บ response หลัง decompression ดังนั้น dictionary compression ให้ประโยชน์เฉพาะครั้งแรกที่ fetch จาก network ครั้งต่อไปที่มาจาก cache จะได้ผลปกติของ cache อยู่แล้ว

Editorial Team
เกี่ยวกับผู้เขียน Editorial Team

Our team of expert writers and editors.