# HTML Size Optimizer

> Shrink first-byte HTML and embedded payloads: trim framework state scripts (Fresh/Next), event data attributes, JSON-LD bloat, and duplicate responsive markup so the client receives less markup and JSON.

- Skill: `decocms/html-size-optimizer` (Agent Skill)
- Install (CLI): `npx skillmds@latest add decocms/html-size-optimizer`
- Raw SKILL.md: https://api.skillmd.com/api/skills/decocms/html-size-optimizer/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: decocms (https://skillmd.com/u/decocms)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/decocms/html-size-optimizer

---


## How to measure HTML size (do this first)

Before hunting for bloat, measure — and measure the **decoded** size, not the transferred one.
gzip/brotli compresses repeated JSON keys heavily, so a bloated page hides on the wire (a listing page
can be ~150 KB brotli but several MB decoded). The Network "transferred" column will look fine.

```js
// in the page console — decoded bytes the browser actually parses:
document.documentElement.outerHTML.length
```

```bash
# same thing from the terminal (‑‑compressed makes curl decode the response):
curl -sS --compressed "https://<site>/<path>" | wc -c
```

Rule of thumb: a page over ~1–2 MB **decoded** is worth investigating — walk the sections below to find
what's inflating it. (In the browser Network tab you can also hover the "Size" cell → it shows
"X transferred over network, Y resource size"; the second number is the decoded size.)

---

## 1. JSON data for the framework

Many frameworks inject large JSON payloads into the HTML, for example:

```html
<script id="__FRSH_STATE_">{}</script>
<script id="__NEXT_DATA__" type="application/json" crossorigin="">{}</script>
```

**What to do:** Reduce JavaScript sent to the client. Pass only the props that client components need.

---

## 2. JSON data for events

Sites sometimes store event-related data in the HTML:

```html
<div data-event="%7B%7D"></div>
```

**What to do:** Send only the data that will actually be used.

### The biggest real-world offender: per-card analytics events on PLPs

The most damaging version of this is an analytics event (GA4 `select_item` /
`view_item_list`, or a `dataLayer.push`) rendered **once per product card** that
serializes the **entire product object** into an inline `<script>`:

```html
<!-- one of these PER CARD — 24+ on a listing page -->
<script>
  (function(){ window.dataLayer.push({ event: "select_item", ecommerce: { items: [
    /* the WHOLE VTEX Product: offers.offers[] (installments per payment method),
       additionalProperty (all specs), isVariantOf.hasVariant (every variant) ... */
  ]}}); })();
</script>
```

The event only reads a handful of fields (`item_id`, `item_name`, `item_brand`,
`price`, `item_category`, `index`), but the full product graph rides along —
offers/installments alone can be **hundreds of KB per card**.

**Why it costs money (egress + compute):** this markup is regenerated by **SSR on
every uncached render** (and bots crawling facet combinations are almost always
uncached), so it burns CPU per render *and* egresses in full on every miss. On a
Deco/VTEX store this was measured at **~1 MB of HTML per card → an `/joias/aneis`
PLP of 11.9 MB decoded HTML (94% was this dead JSON)**.

**What to do:** build a minimal item with only the fields the event consumes and
pass *that* to the handler — never the raw product.

```tsx
// before: the whole product is serialized into the card's <script>
<SendSelectItemEvent product={product} />

// after: only what GA4 reads
<SendSelectItemEvent item={{
  productID: product.productID,
  inProductGroupWithID: product.inProductGroupWithID,
  name: product.name,
  brand: product.brand?.name,
  url: product.url,
  category: product.category,
}} />
```

Result on the store above: PLP HTML **11.9 MB → 0.63 MB (~18×)**, tracking
identical (same `dataLayer` event, field for field).

**How to spot this specific case:** after measuring the decoded size (see "How to measure" above),
grep for a marker that only exists in the serialized offers — on VTEX, `billingIncrement`:

```js
(document.documentElement.outerHTML.match(/billingIncrement/g) || []).length
```

A nonzero count on a listing page means the full offers/installments object is being serialized per card.

---

## 3. JSON for SEO structured data

```html
<script type="application/ld+json">{
  "@context": "https://schema.org",
  "@graph": []
}</script>
```

**What to do:** Include only the structured data that is valid and used; avoid large or redundant objects.

---

## 4. Hidden elements for responsiveness

Sites sometimes hide whole blocks per breakpoint, doubling (or more) the HTML:

```html
<header class="hidden md:block">a lot of HTML</header>
<header class="block md:hidden">a lot of HTML</header>
```

**What to do:** Serve only the HTML needed for the requesting device (e.g. split or choose on the server).

