# Performance / Core Web Vitals Audit — Quire Guide (quire.io/guide/)

**Date:** 2026-07-02
**Method:** Lab measurement only. PageSpeed Insights / CrUX API were unavailable (no Google API key configured in this environment; the shared/keyless PSI quota was exhausted — `PSI rate limit exceeded (240 QPM / 25,000 QPD)`). No Lighthouse CLI installed. Measurements below were captured with a custom Playwright (headless Chromium) script that:
- Hooks the real `PerformanceObserver` APIs in-page for **LCP** (largest-contentful-paint), **CLS** (layout-shift), and long tasks.
- Records the full network waterfall (status, type, size, cache headers) via CDP response events.
- Runs single-shot, single-device (1350×940 desktop viewport), warm-DNS, cold-cache loads from one network location (not a 75th-percentile CrUX field sample).

**What this means for confidence:**
- LCP and CLS below are **measured** (real browser APIs), but from a **single lab run per page**, not the 28-day, 75th-percentile CrUX field distribution Google actually scores against. Treat them as directional, not authoritative.
- **INP is unavailable** — INP requires a real user interaction (click/tap/keypress) sampled across real sessions; it cannot be produced by a scripted single-page load with no interaction, and no field data source was reachable. No interaction-heavy widgets were found on these pages during inspection (no client-side search-as-you-type, no complex JS widgets on scroll), so INP risk is judged qualitatively as low, not measured.
- Page weight, request counts, render-blocking resources, image sizing, third-party scripts, and caching headers below are **directly measured** from the network waterfall / HTTP response headers.

## Pages tested
1. `https://quire.io/guide/` (homepage)
2. `https://quire.io/guide/add-task/` (short action article)
3. `https://quire.io/guide/agile-scrum/` (long template article, image-heavy)
4. `https://quire.io/guide/charts/` (feature article, image-heavy)

## Results summary

| Page | TTFB | FCP | LCP (lab) | CLS (lab) | Load event | Total requests | Total bytes (with Content-Length) | DOM nodes |
|---|---|---|---|---|---|---|---|---|
| /guide/ | 780ms | 1184ms | **1652ms** | **0** | 1938ms | 47 | ~910 KB | 696 |
| /guide/add-task/ | 488ms | 1044ms | **1540ms** | **0** | 1618ms | 48 | ~802 KB | 826 |
| /guide/agile-scrum/ | 483ms | 1052ms | **1532ms** | **0** | 1744ms | 51 | ~999 KB | 906 |
| /guide/charts/ | 667ms | 1084ms | **1588ms** | **0** | 1797ms | 49 | ~1006 KB | 799 |

**CWV verdict (lab, single run, desktop):**
- **LCP: PASS** on all 4 pages — 1.5–1.65s, well under the 2.5s "good" threshold. Caveat: this is a single desktop lab run on a fast connection with no throttling; real-world mobile/3G field data (75th percentile) is very likely to be materially slower, particularly given TTFB of 480–780ms and 800KB–1MB of blocking-adjacent payload. Cannot confirm CrUX pass/fail without field data.
- **CLS: PASS (lab)** — 0 measured shift on all 4 pages during and after load. However, **no `<img>` tag on any page carries `width`/`height` attributes**, which is a structural CLS risk under real-world conditions (slower connections, cache misses, different viewport/font-swap timing can all surface shifts that didn't appear in this fast, warm-lab run). This should be fixed defensively even though it isn't currently manifesting as a lab CLS problem.
- **INP: Not measured** — no interaction sampled. Given DOM sizes are moderate (696–906 elements, under the 1,500-element risk threshold) and no long tasks were observed during page load (0 long tasks >50ms on every page), INP risk appears low, but this is a qualitative judgment, not a measurement.

## Detailed findings

### 1. TTFB is inconsistent and occasionally high (homepage: 780ms)
The homepage's TTFB (780ms) is close to the "needs improvement" boundary (800ms) for the CrUX TTFB subpart, while inner article pages are much faster (480–670ms). Server: `nginx`, static Jekyll build, so this is likely first-byte overhead from a shared/loaded backend or lack of edge caching/CDN in front of `quire.io`, not template rendering (there is none — it's static HTML).

### 2. Static assets have no `Cache-Control` / `Expires` headers (repeat-visit tax)
Verified directly via HTTP headers on multiple asset types:
```
GET /guide/assets/css/main.css  -> last-modified, etag only, NO cache-control, NO expires
GET /guide/assets/js/main.min.js -> NO cache-control
```
Across the full waterfall, **every first-party static asset** (CSS, JS bundles, PNG screenshots, SVGs, the custom icon font `QuireIcons.woff`) was served with only `ETag`/`Last-Modified` and no `Cache-Control` or `Expires` header. This forces the browser to issue conditional (304) revalidation requests for every asset on every repeat visit instead of reading from disk cache, adding avoidable latency and server load for the ~13 help-center languages' worth of repeat traffic (support agents, returning users navigating between guide articles).

### 3. All content images are unoptimized PNG — no WebP/AVIF, no responsive `srcset`
Every screenshot across all 4 pages (`add_task.png`, `charts.png`, `main_list_backlog.png`, etc.) is a PNG, typically 700-760px wide, 20–45 KB each. None use WebP or AVIF, and none use `srcset`/`<picture>` for responsive sizing. Modern re-encoding (WebP or AVIF, quality ~75-80) typically cuts screenshot payload 40-60% with no visible quality loss. Combined image payload per article page is 130-340 KB; this is the single largest easily-actionable byte-savings opportunity on the site.

### 4. Homepage thumbnail images are served 2.8× larger than displayed (no `srcset`, no explicit sizing)
On `/guide/`, six thumbnail images (`thumbnail_sublist.png`, `thumbnail_custom_field.png`, `thumbnail_formula.png`, `thumbnail_peekaboo.png`, `thumbnail_mobile.png`, `thumbnail_time_tracker.png`) are natively 608×420px but displayed at only 214×148px — roughly 2.8× oversized in both dimensions (~8× the pixel area actually needed). The footer logo SVG (`quire_footer_white.svg`) is also displayed at 20×20 despite being a 141×150 source (SVG so the byte cost is trivial, but it's a sign of the same missing-responsive-sizing pattern).

### 5. No `width`/`height` attributes on any `<img>` element (CLS risk not yet manifesting)
Every image on every page tested — hero screenshots and thumbnails alike — omits `width`/`height` HTML attributes, relying entirely on CSS/container sizing to reserve space. In this lab run CLS measured 0 throughout, but this is a fragile, environment-dependent result: on slower connections, or before CSS finishes loading, the browser has no intrinsic size to reserve, and shifts can appear in the field even when they don't in a fast synthetic run. Chrome's own image-CLS heuristic (auto aspect-ratio from `width`/`height` attrs) is not benefiting these images at all right now.

### 6. Heavy third-party analytics/ad-tag footprint (23–24 third-party requests per page, ~50% of all requests)
Every page loads a large marketing/analytics stack that fires on every pageview:
- Google Tag Manager (2 separate GA4/UA container IDs)
- Google Ads conversion tracking (2 separate AW- IDs, each firing multiple `googleads.g.doubleclick.net`, `google.com/pagead/1p-user-list`, `google.com/ccm/collect`, `google.com/rmkt/collect` remarketing/audience-list pixels)
- Microsoft/Bing Ads UET tag (`bat.bing.com/bat.js` + `bat.bing.com/p/action/...js` + `bat.bing.com/action/0`)
- `doubleclick.net`, `analytics.google.com`, `stats.g.doubleclick.net` collect/beacon calls

This amounts to **23–24 of the ~47-51 total requests per page** (roughly half), none of which contribute to page content. While most of these are asynchronous `fetch`/`ping`/pixel calls that don't block rendering (0 long tasks were observed, and LCP/FCP were not delayed by them in this run), this is a disproportionate amount of ad/remarketing tracking for a documentation/help-center site, and duplicated conversion tags (both `AW-1067329176` and `AW-11044828705`, plus both `UA-48347893-1` and `G-VDRZSEZ5M5`) suggest redundant/legacy tags that were never cleaned up. This is worth a marketing-ops review independent of the direct performance impact.

### 7. Render-blocking CSS chain (4 stylesheets, including a cross-origin Google Fonts CSS import)
All 4 pages block first paint on the same 4 synchronously-loaded stylesheets in `<head>`:
1. `fonts.googleapis.com/css2?family=Inter...` (cross-origin, adds a DNS+connection+download round trip before FCP)
2. `quire.io/s/css/bootstrap.min.css`
3. `quire.io/s/css/blog.css`
4. `quire.io/guide/assets/css/main.css`

Plus one render-blocking script in `<head>` (`assets/js/lang-auto.js`, not marked `async`/`defer`). None of this is currently pushing LCP into "poor" territory in this lab run, but the Google Fonts stylesheet in particular adds an avoidable third-party round-trip ahead of first paint, and consolidating `bootstrap.min.css` + `blog.css` + `main.css` (62KB combined) would reduce request count and connection overhead.

## What works well

- **LCP is comfortably in the "good" range** (1.5–1.65s) across all tested pages in this lab environment — no evidence of the 4s+ "poor" LCP problems common on media-heavy sites.
- **No long tasks (>50ms) observed on any page during load** — no evidence of main-thread-blocking JavaScript that would threaten INP, though INP itself could not be directly measured.
- **DOM size is reasonable** (696–906 elements), well under the 1,500-element bloat threshold that typically correlates with INP problems.
- **Compression is applied correctly** — HTML, CSS, and JS assets are served gzip/brotli-encoded (`content-encoding: gzip`/`br` confirmed on all text assets).
- **No CLS observed** in any lab run, despite the missing `width`/`height` attributes — current templates/CSS are apparently sizing containers well enough to avoid visible shift in this environment.
- **Static site architecture** (Jekyll, no server-side rendering/database per-request) keeps TTFB in a reasonable range (480–780ms) without any application-layer bottleneck.

## Prioritized recommendations

| Priority | Recommendation | Expected impact |
|---|---|---|
| High | Add `Cache-Control: public, max-age=31536000, immutable` (with cache-busting filenames/query hashes) to all static assets (CSS, JS, images, fonts) served by nginx. | Eliminates repeat-visit revalidation round-trips for the ~50 assets/page across all repeat visits and all 13 locales; reduces server load. |
| High | Re-encode all guide screenshots to WebP (with PNG fallback via `<picture>` or `Accept`-based content negotiation) and generate correctly-sized homepage thumbnails (214×148 instead of shipping 608×420 and scaling down in CSS). | 40-60% reduction in image payload (largest single byte-savings opportunity, ~130-340 KB/page); improves LCP margin especially on slower/mobile connections not captured by this lab run. |
| Medium | Add explicit `width` and `height` attributes to every `<img>` tag across all guide templates. | Removes CLS risk under real-world/slow-network conditions; free win, no visual change. |
| Medium | Reduce/consolidate marketing tag footprint: confirm whether both `AW-1067329176`/`AW-11044828705` and both `UA-48347893-1`/`G-VDRZSEZ5M5` are still needed; remove duplicates. | Cuts ~10-12 redundant third-party requests per page; reduces tracking-related privacy/compliance surface as a side benefit. |
| Medium | Self-host or preconnect+swap the Google Fonts `Inter` stylesheet (or inline critical `@font-face` with `font-display: swap`, already implied by the `display=swap` param) and consider bundling `bootstrap.min.css` + `blog.css` + `main.css` into one file to cut render-blocking round trips. | Shaves latency off the critical rendering path ahead of FCP/LCP. |
| Low | Investigate homepage TTFB (780ms) vs. inner-page TTFB (480-670ms) — check whether a CDN/edge cache sits in front of `quire.io/guide/` and whether the homepage is being regenerated or fetched from a colder cache tier than article pages. | Tightens TTFB margin against the 800ms CrUX "good" threshold. |
| Info | Re-run this audit with real PageSpeed Insights/CrUX API access (needs a Google API key) once available, and capture field INP data — current results are lab-only and cannot substitute for 75th-percentile field validation. | Closes the measurement gap called out throughout this report. |
