Optimize Post Lists and Cache Invalidation: Lessons from PR #128

Frank Mendez·
Optimize Post Lists and Cache Invalidation: Lessons from PR #128

A practical guide to smaller list queries and precise cache refreshes. PR #128 offers a case study in pagination, summary projections, authorization, measurement, and tradeoffs that apply to any content-heavy app.

A post list can look simple on screen while doing expensive work behind it. If every row downloads the full article body, joins editor-only fields, and then filters and sorts in the browser, the page pays for data it never displays. When a post changes, clearing a broad cache may hide the symptoms while forcing unrelated pages to reload.

PR #128, “Paginate post lists and streamline publication refreshes,” is a useful case study in addressing both problems. Its techniques apply to product catalogs, issue trackers, admin tables, documentation sites, and other systems where lists and detail pages need different data and freshness rules.

1. Make the list query match the list UI

A list row usually needs a title, slug, status, author name, category, update time, and perhaps a short excerpt. It rarely needs the entire article body, SEO metadata, or all editor state. Define a small summary projection for listing pages instead of selecting every column and trimming the result afterward.

Then bound the work at the data source. Apply search, filters, ordering, and pagination in the database or API, and return only the current page. A browser-side slice is not pagination if the server already sent every row. Stable ordering also matters: sort by the visible date and add a unique tie-breaker such as the record ID. Without that tie-breaker, equal timestamps can move between pages as data changes.

Exact totals and filter choices need deliberate handling too. Deriving category filters from the current page makes options disappear as users paginate. Calculate facets independently or return them with the server query. Exact counts are useful, but can be expensive at scale; measure them and consider a “has next page” contract or approximate totals when the product does not need a precise number.

For an editor, use a separate, authorized query that loads the full content only after the user opens a post. Different screens have different data contracts. Keeping those contracts separate reduces response size and makes it easier to reason about privacy, caching, and database indexes.

2. Keep filtering and pagination state reproducible

Search, category, sort direction, page size, and page number are useful in the URL. URL-backed state makes a list shareable and lets refresh, Back, and Forward reproduce what the user saw. Parse query parameters defensively: clamp page sizes, reject invalid page numbers, bound search length, and allowlist sort columns and directions.

The database query should use the validated state, not a client-side approximation. Tests should cover the full round trip from URL to parsed state to query parameters, including malformed and repeated values. This is especially important when the list can grow past an API’s default row cap.

3. Centralize cache invalidation around content events

Publication changes affect several surfaces: the home page, article route, blog index, category and tag pages, author pages, and sitemap. Scattering refresh calls across create, update, rename, unpublish, and delete handlers makes omissions likely. One handler eventually forgets the old slug, or a new taxonomy route never gets refreshed.

A small cache module can define the dependency map in one place. Give it semantic inputs such as the previous and next slug, category, and author; have it refresh only the routes that depend on those values. Draft edits can use a much smaller rule, such as refreshing the dashboard list without touching public pages. When a slug changes, invalidate both the old and new URLs. When a post is unpublished or deleted, invalidate the route that used to serve it as well as the relevant list surfaces.

This approach scales beyond Next.js. The underlying idea is a dependency graph: a content event identifies which cached representations may now be stale. In a tag-based cache, invalidate the tags; in a CDN, purge the affected keys; in a framework path cache, revalidate the affected paths. Keep the event-to-dependency policy centralized, and make it easy to test.

Avoid invalidating the root layout or the entire site for every content edit. Broad invalidation can turn a small change into extra database reads, rendering work, and slower requests for unrelated pages. Precision is valuable only when the dependency map is correct, so test warm-cache behavior after create, edit, rename, category or tag changes, unpublish, and delete.

4. Optimize derived fields without making writes fragile

Lists often need derived values such as reading time. Computing the same value repeatedly in every request wastes work; storing or generating it can move that cost elsewhere. A generated database column keeps the value consistent and makes it queryable, but the calculation becomes part of every write and possibly a table-rewriting migration.

That is a reliability boundary. The derived value should never prevent the source content from being saved. Test malformed and unusual inputs, add migration checks for existing rows, and estimate the rewrite time for production-sized tables before deployment. A convenience optimization is not worthwhile if one rare input can block publishing or fail a migration.

5. Parallelize independent reads, preserve write ordering

Publication flows often perform independent checks, data hydration, and authorization-gated reads. Running independent reads concurrently can reduce waiting time. Keep validation before mutation, preserve ownership checks before privileged access, and do not parallelize steps whose order protects atomic saves, optimistic concurrency, URL reservations, or newsletter handoff behavior. Faster orchestration should not weaken the guarantees the workflow already depends on.

What PR #128 measured

The PR reports a local benchmark using 1,500 posts. Dashboard query JSON fell from about 12.9 MB to 7.3 KB, a 99.94% reduction; public list JSON fell by 93.59%. Median dashboard readiness fell from 637 ms to 160 ms. The author explicitly does not claim a timing improvement for editor or publish flows. Those measurements illustrate the effect of requesting summaries and bounded pages; they are specific to that benchmark and should not be treated as universal expectations.

The PR also reports database checks with 1,502 posts, browser verification with 1,005 posts, and publication tests against warmed public pages. The general lesson is to test at the scale and state transitions that expose the problem: beyond row caps, across page navigation, and after caches have already been populated.

Tradeoffs to review before adopting the pattern

As of October 12, 2026, PR #128 is open. Its review identifies two decisions worth understanding. First, the generated reading-time calculation needs a safe fallback for JSON text that cannot be cast to jsonb, including a NUL escape; otherwise a stored generated column can reject a content write or break a migration. Add a regression case and ensure a derived metric cannot become a write prerequisite.

Second, the PR uses dynamic rendering on cookie-dependent public routes to avoid production errors for URLs created after a build. That fixes a correctness issue, but dynamic rendering can give up static regeneration and add a database read to page requests. A cookie-free public read path that still observes anonymous RLS may preserve static caching, if the data access design supports it. Choose the cache model intentionally and measure both freshness and request cost.

A reusable checklist

  • Return only fields the current screen needs.

  • Filter, sort, count, and paginate at the data source; use a stable order.

  • Keep public summaries separate from authorized editor queries.

  • Parse list state from URLs with strict bounds and allowlists.

  • Centralize route or cache-tag dependencies for publication events.

  • Invalidate both old and new identities when slugs or taxonomy change.

  • Test warm-cache transitions and realistic row counts.

  • Make derived fields resilient so they cannot block source writes.

  • Compare payload size, query time, request count, and end-to-end readiness.

Optimizing a content list is not just a matter of adding an index or a loading spinner. The strongest result comes from sending less data, doing bounded work where the data lives, and refreshing only the representations that changed. PR #128 demonstrates those ideas in one CMS; the same design questions apply anywhere an application serves both searchable lists and frequently changing detail pages.

Source

PR #128: Paginate post lists and streamline publication refreshes

Stay in the loop

Get notified when new posts are published. No spam, unsubscribe anytime.

No spam · Unsubscribe anytime

💬 Leave a Comment

Want to join the conversation?