mrkeyoor.com_
Thu 06 Aug 01:00 UTC
npmUtilsupdated 05 Aug 2026

lz-string

lz-string compresses a JavaScript string into a shorter JavaScript string, with no dependencies and about 1.4 KB gzipped of code. It is an LZW variant tuned for the fact that browsers store strings as UTF-16, so it can pack more than 8 bits into every output character. You pick an output flavor depending on where the result is going: compressToUTF16 for localStorage, compressToEncodedURIComponent for URLs, compressToBase64 for anything that expects ASCII, compressToUint8Array for binary transport. Decompression is the matching function on the other side. It only handles strings, so objects go through JSON.stringify first.

Verdict

Still the shortest path from a fat JSON blob to a localStorage key or a shareable URL, and 1.4 KB is hard to argue with. But it has not shipped a release since March 2023, and if your targets support CompressionStream you are paying bundle bytes for something the platform now does better.

API stability5/5The ten exported functions have not changed since 1.4, and 1.5.0 in March 2023 was the last publish, so there is nothing left that could move under you.
Docs2/5The README is mostly a list of language ports plus CLI help, and the real documentation lives on an http-only personal blog page; nothing published explains when the output grows instead of shrinks or why compress() output cannot go over the network.
Maintenance2/5Last npm publish March 2023, last repo push August 2025, 43 open issues on top of open PRs, and an unreleased version 2 sitting on master that the README already documents as if it shipped.
Ecosystem4/5About 57.1M weekly downloads, ports in a dozen languages, and it turns up inside analytics and state-sharing libraries; mindshare is drifting toward the native CompressionStream API.

Use it if

  • You are hitting the roughly 5 MB localStorage or sessionStorage quota and the data is repetitive JSON, where this typically cuts it by more than half
  • You want to put application state in a URL or query string and need it to survive copy-paste: compressToEncodedURIComponent emits only URL-safe characters, so no extra encodeURIComponent pass
  • You need something that runs identically in a browser, in a service worker, and in Node with zero build config and zero dependencies
  • You want the compression step to be synchronous, because CompressionStream is promise-based and awkward inside a synchronous storage wrapper
Skip it if

Setup reality

Install is npm install lz-string and there is nothing to configure, but the published 1.5.0 tarball is a UMD file with main pointing at libs/lz-string.js, no exports map, no ESM build, and no package type field. Bundlers handle it fine but they cannot tree-shake it, so you ship all ten functions even if you call one. The bundled typings declare named exports only, so a default import fails typecheck under TypeScript unless allowSyntheticDefaultImports is on; use named imports or import star. The README on GitHub describes a version 2 layout with separate CommonJS and ESM entries, which is not what npm gives you. A global install also puts an lz-string CLI on your PATH.

Patterns

Store a compressed value in localStoragecompress-for-localstorage

import { compressToUTF16, decompressFromUTF16 } from 'lz-string';

localStorage.setItem('draft', compressToUTF16(JSON.stringify(draft)));

const raw = localStorage.getItem('draft');
const draft = raw ? JSON.parse(decompressFromUTF16(raw)) : null;

Use the UTF16 pair here, not plain compress(). compress() emits unpaired surrogates that survive localStorage in most engines but get mangled the moment the value touches JSON, fetch, or a UTF-8 file.

Put state in a query string or hashcompress-for-url

import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from 'lz-string';

const param = compressToEncodedURIComponent(JSON.stringify(filters));
history.replaceState(null, '', `?s=${param}`);

const s = new URLSearchParams(location.search).get('s');
const filters = s ? JSON.parse(decompressFromEncodedURIComponent(s)) : {};

The output already uses only URL-safe characters, so do not wrap it in encodeURIComponent; doing so escapes nothing but does add length, and a stray decodeURIComponent on the way back can turn + into a space and break decompression.

Only keep the compressed form if it is smallerguard-against-growth

import { compressToUTF16 } from 'lz-string';

function pack(text) {
  const packed = compressToUTF16(text);
  return packed.length < text.length
    ? { z: 1, v: packed }
    : { z: 0, v: text };
}

Short or high-entropy strings compress to something longer than the input. A one-byte flag costs nothing and stops you from paying for compression that made things worse.

Handle corrupt or foreign inputdetect-decompress-failure

import { decompressFromUTF16 } from 'lz-string';

function safeLoad(raw) {
  let text;
  try {
    text = decompressFromUTF16(raw);
  } catch {
    return null;
  }
  if (text === null || text === '') return null;
  try {
    return JSON.parse(text);
  } catch {
    return null;
  }
}

Decompressing garbage does not reliably throw: you can get null, an empty string, or plausible-looking mojibake, so always validate the result rather than trusting an absent exception.

Produce plain ASCII outputcompress-to-base64

import { compressToBase64, decompressFromBase64 } from 'lz-string';

const body = compressToBase64(JSON.stringify(payload));
await fetch('/api/report', { method: 'POST', body });

This is the flavor to use whenever the value crosses a UTF-8 boundary: HTTP bodies, cookies, JSON fields, log lines. It is roughly 33 percent bigger than the UTF16 flavor for the same input.

Get bytes for binary transportcompress-to-uint8array

import { compressToUint8Array, decompressFromUint8Array } from 'lz-string';

const bytes = compressToUint8Array(JSON.stringify(payload));
ws.send(bytes);

// on the other side
const text = decompressFromUint8Array(new Uint8Array(event.data));

The 1.5.0 encoder pads to an even byte length, which is why the repo CLI has a --legacy flag; if you decompress with a different language port, check that it agrees on odd-length handling.

Import it without a typecheck errortypescript-import

// works: the bundled typings declare named exports only
import { compress, decompress } from 'lz-string';

// also works
import * as LZString from 'lz-string';

// fails typecheck unless allowSyntheticDefaultImports is on
// import LZString from 'lz-string';

typings/lz-string.d.ts has no default export even though the runtime module.exports is a single object, so the default import is a compile-time problem, not a runtime one.

A drop-in compressed storage wrapperwrap-storage-api

import { compressToUTF16, decompressFromUTF16 } from 'lz-string';

export const zstore = {
  set(key, value) {
    localStorage.setItem(key, compressToUTF16(JSON.stringify(value)));
  },
  get(key, fallback = null) {
    const raw = localStorage.getItem(key);
    if (raw == null) return fallback;
    const text = decompressFromUTF16(raw);
    return text ? JSON.parse(text) : fallback;
  },
};

Namespace your keys or bump a version prefix when you change the format: existing users still have values written by the old scheme and there is no header telling you which is which.

Check the ratio on your real payloadmeasure-before-adopting

import { compressToUTF16 } from 'lz-string';

const sample = JSON.stringify(realWorldState);
const packed = compressToUTF16(sample);
console.log({
  chars: sample.length,
  packedChars: packed.length,
  ratio: (packed.length / sample.length).toFixed(2),
});

Compare against the native path before committing: repetitive JSON often lands near 0.3, while already-compressed or random data lands above 1.0.

The platform alternative worth measuring againstnative-compressionstream-fallback

async function gzipToBase64(text) {
  const cs = new CompressionStream('gzip');
  const stream = new Blob([text]).stream().pipeThrough(cs);
  const buf = await new Response(stream).arrayBuffer();
  return btoa(String.fromCharCode(...new Uint8Array(buf)));
}

Available in Node 18+ and current browsers, costs no bundle bytes, and usually wins on larger payloads; the trade-off is that it is async, which makes it awkward inside a synchronous storage shim.

Compress a file from the shellcli-compress-file

npm install -g lz-string
lz-string -e base64 input.json -o output.txt
lz-string -d -e base64 output.txt

The CLI ships in the published tarball at bin/bin.js; -e picks the encoder and it must match on both ends, since raw and base64 output are not interchangeable.

Alternatives

PackageRegistryPick it when
fflatenpmYou want real DEFLATE with gzip and zip support, still small, and you need it to interoperate with server-side tooling
pakonpmYou need a full zlib port in JavaScript with streaming and tuning knobs, and bundle size is not the constraint
lzutf8npmYour input is UTF-8 heavy text and you want async and worker-based compression out of the box