hammerstone/fast-paginate
Laravel pagination macro for large tables: swap paginate() for fastPaginate() and it fetches just the IDs for the current page via a subquery, then joins them back—avoiding slow deep offsets. Drop-in replacement for Eloquent builders, no config or view changes needed.
fastPaginate() is a drop-in swap for paginate() that removes the SELECT COUNT(*) that Laravel runs on every paginated request, replacing it with a bounded subquery (... LIMIT offset+perPage+1). This lets a PM decide between three options with real data: (a) ship a one-line package change this sprint, (b) schedule a cursor-pagination redesign next quarter, or (c) invest in read replicas / count caching. The package buys time for (b) at near-zero cost, which is usually the correct sequencing.total and last_page. That is a product question, not an engineering one. Numbered page links ("Page 47 of 312"), "showing 1–20 of 4,382 results," and CSV export-by-page-count all depend on the count query. Deciding to drop or defer those is a roadmap call the package surfaces.Adopt when most of the following hold:
COUNT(*) is the dominant cost — typically tables in the millions of rows, or queries with expensive WHERE/JOIN clauses where the count has to scan the same working set as the page.total / last_page. "Next" and "Previous" affordances are acceptable.WHERE/ORDER BY, or joins that survive being wrapped in a LIMIT-bounded subquery.Look elsewhere when any of these are true:
LIMIT 20 OFFSET 200000 is still slow with or without a count. That is a cursor/keyset pagination problem (cursorPaginate() is built into Laravel), not a counting problem. Adopting this package here treats a symptom.groupBy, having, distinct, unions, or CTEs where wrapping the select in a bounded subquery changes semantics or fails outright. Budget engineering validation time for each paginated endpoint, not a blanket find-and-replace.total(), lastPage(), page-number link rendering, onEachSide, and withQueryString interactions all need review. Treat "same interface" as approximately true, not exactly true.To executives: We have a class of API endpoints that are slow and expensive purely because of a row-count query Laravel runs on every page load — a query whose result we display but rarely act on. This package replaces that count with a bounded query that stops scanning as soon as it has enough rows to know whether a next page exists. The change is roughly one line per endpoint, requires no schema or client changes in the common case, is MIT-licensed, and is small enough that we could maintain it ourselves. The trade is that we lose exact result totals on those screens — a product concession, not a technical one — which we should confirm per surface before rollout. Expected outcome: lower database cost and faster page loads on our heaviest list views, delivered in days, while we scope a proper cursor-pagination redesign for the long term without an outage forcing our hand. Risk is low and reversible: the change is per-query and can be reverted individually.
To engineering: This is a maintenance-light dependency, not a framework commitment. It exposes fastPaginate() as a builder macro; the implementation wraps the existing query in a LIMIT offset+perPage+1 subquery and counts that, so the database short-circuits instead of scanning the full working set. Things to verify before rollout: endpoints that read total()/lastPage() or render numbered links; any groupBy/having/distinct queries, which may not wrap cleanly; and tests asserting on pagination metadata. The package is small, MIT, and easy to vendor or fork if the maintainer goes quiet, so treat it as an internal utility with an upstream — not a strategic bet. Measure before/after with query timing and DB CPU on two or three representative endpoints; if the count wasn't the bottleneck, roll it back and we've learned something cheap.
How can I help you explore Laravel packages today?