Skip to content
Shops & CMS

Pagination, Load More or Infinite Scroll: Fast Listings

Pagination, load more or infinite scroll in shop listings: how initial load, INP, layout shifts, returning to the same spot and a URL per page fit together.

21 min read KategorieseitePaginationLadezeitShopware

In short and in plain language

Many shops have long lists of products. The list does not load all at once, but in parts. That keeps the start fast.

There are three ways. You use page numbers. You press a button for more products. Or new products appear by themselves as you scroll.

When you go back from a product to the list, you should land in the same spot. For this, every part of the list needs its own address.

Nothing should jump when more products load. So the page keeps space free for the new products.

We check your category pages. Then we set up the loading so that the pages stay fast.

A category page with a few hundred products does not load in one piece, and that is a good thing. Google explicitly names a faster initial page load, compared with loading all results at once, as a benefit of incremental loading (Google Search Central). What remains open is how the next products arrive: through page numbers, through a "Load more" button or automatically while scrolling. The choice goes beyond looks. It determines how much script the page needs at the start, whether newly loaded tiles shift the content, whether the back button returns to the same spot and whether every section has its own address. This article compares the three patterns on exactly these points, explains what the item count per loading step depends on and shows why deep pages put more load on the database than the first one. Where Shopware is concerned, we rely on the storefront source code in version 6.7.15.1.

Key takeaways

  • Initial load depends mostly on section size: out of the box, Shopware loads 24 products per page (Shopware source code). The right size is set by the weakest device, usually the smartphone.
  • Pagination works with plain links and no script. "Load more" and infinite scroll need a script that reserves space, manages focus and keeps the address in sync through the History API.
  • Scrolling does not excuse a layout shift (web.dev): with infinite scroll, every shift caused by new tiles counts in full; after a "Load more" click, shifts count once 500 milliseconds have passed (web.dev). Tile-sized placeholders prevent both.
  • Anyone who lands at the top of the list after going back has to find their position again, and that affects many journeys: on mobile devices, 1 in 5 navigations is a back or forward navigation (web.dev). The fix: stay bfcache-eligible and keep the section in the address, such as ?p=3.
  • Every page in a series needs its own URL and its own canonical link instead of pointing to page 1. Google's crawlers follow links but do not click buttons (Google Search Central).
  • Deep pages cost more: with OFFSET, the database reads every skipped row (MariaDB documentation), and Elasticsearch pages through at most 10,000 hits with from and size by default (Elastic documentation).

Three Patterns, One Goal: Load Less at Once

Google describes three patterns for showing a long list in parts. With pagination, users move between pages that each show a subset through links such as "next", "previous" and page numbers. With "Load more", a button extends the displayed set. With infinite scroll, new content loads as soon as users reach the end of the page (Google Search Central). All three have in common that the full catalog does not arrive at once. Besides the faster initial load, Google names reduced network traffic, which matters especially on mobile devices, and lower backend load, because less content is read from the database (Google Search Central).

The differences lie in the details, and Google names them itself: pagination shows how large the result set is and where users are, but requires more controls and a new page load for every further page. "Load more" keeps everything on one page and can state the total size on the button, but struggles with very large numbers of results because everything ends up on the same page. Infinite scroll is intuitive, but can lead to scrolling fatigue when the size of the result set is unclear (Google Search Central). For page speed, four questions come on top: What loads at the start? Does loading more shift the content or slow down the response to clicks? Where does the back button lead? And does every section have its own address?

Scope

How filters and facets in a listing respond quickly is covered in the article Filters in the shop listing. Why many nodes in the tree make every interaction more expensive is explained in Reducing DOM size, and the basics of the instant back navigation in the article on the back/forward cache. This article is about choosing the loading pattern itself.

Initial Load: Section Size Decides

With the same number of products per section, all three patterns load the same product data on the first visit. The initial load still differs, namely in script and preloading. Pagination gets by with ordinary links, and the page works before any script has run. "Load more" needs a small script that fetches and inserts the next section. Infinite scroll additionally needs an observer at the end of the list, usually preloads the next section before anyone sees it and has to keep the address up to date. The mockup above shows this order schematically. The bigger lever, however, is the number of products per section: every tile brings an image, a price, markup and often its own scripts for wish lists or quick views.

In Shopware, the setting "Number of products per page" defines this number per sales channel. Without a value, the default of 24 products applies (Shopware source code). Through the limit parameter, at most 100 products can be requested per call (Shopware source code). 24 is a practical size because it divides by two, three, four and six: the last row is full in every common grid, and there are no half rows that stand out when more products load.

There is no single number that suits every shop. The trade-off is clear, though: fewer products per section mean less data and fewer images to render on the first load, while too few force constant paging. One workable approach is to combine "Load more" with lazy loading of product images: the section can then be larger without raising the initial load to the same degree. The field data of your own category pages shows which size works in your shop.

The weakest device sets the section size. 73 percent of online shoppers aged 16 and over in Germany shop on a smartphone (Bitkom 2025), and secondary pages perform worse on mobile in particular: only 69 percent of mobile secondary pages achieve a good INP, compared with 80 percent of home pages (HTTP Archive Web Almanac 2025). Category pages belong to these secondary pages. A fixed number per address still makes sense, even if desktop and smartphone can handle different amounts: if you want to show more on desktop, load the second section there automatically instead of changing the page size per device. That way, the same address shows the same products everywhere.

  • Images in the first visible row load right away; all others carry loading="lazy". How lazy loading and code splitting work together is covered in Lazy loading done right.
  • Every tile has fixed dimensions for image and text. On 62 percent of mobile pages, at least one image lacks dimensions (HTTP Archive Web Almanac 2025), and exactly such images push newly loaded rows around.
  • Wish list, quick view and variant selection in the tile start on interaction, not for every tile during loading.
  • The markup of a tile stays flat, because it repeats with every section.

Pagination: the Robust Default

Pagination is the simplest pattern for browsers and search engines. Every page is a separate document that works without script, and every page has the same manageable size. Google recommends linking from each page to the next with <a href> so that the crawler finds the following pages (Google Search Central).

Shopware's default theme takes this route and extends it. The pagination template generates real links with the ?p= parameter and shows only a window of page numbers around the current page. When someone clicks a number, the listing plugin intercepts the click, loads the section with fetch, swaps the tiles and writes the new address to the history with pushState (Shopware source code). The route that delivers the section answers GET requests, is enabled for the HTTP cache and sends x-robots-tag: noindex so the partial response does not end up in the index (Shopware source code). Shopware answers a page number beyond the last page with a "not found" error instead of delivering an empty list (Shopware source code).

The downside is the extra click and, without script, a full page change. That can be shortened: Google advises considering preload, preconnect or prefetch for the move to the next page (Google Search Central). With Speculation Rules, the browser preloads the next page as soon as someone moves the pointer over the link; how this works and where its limits are is explained in the article on Speculation Rules. For usability, the page numbers also need to be large enough to tap on a smartphone, and the current page needs to be marked as such. The Shopware template sets aria-current="page" for this (Shopware source code).

"Load More": Control with Reserved Space

With a "Load more" button, the page stays in place and the next section is appended below. The Nielsen Norman Group sees this as a compromise: the button keeps the footer reachable, makes it clear that there is more content and helps on slow connections because less loads up front. In return, every section costs an extra click that briefly interrupts the flow (Nielsen Norman Group 2022). For search engines, the button is invisible at first: Google's crawlers do not click buttons and generally do not trigger script functions that require a user action (Google Search Central). That is why the button is, in the HTML, a link to the next section, for example ?p=2, which a script turns into a load-more button only in the browser.

For Cumulative Layout Shift, the click is an advantage. Layout shifts within 500 milliseconds of user input carry a flag and can be excluded from the calculation (web.dev). If the server takes longer, the shift counts when the new tiles push the footer down or images without dimensions slide in. For interactions that trigger a longer network request, web.dev recommends creating space right away and showing a loading indicator (web.dev). For "Load more", this means: right at the click, tile-sized placeholders appear for the whole section, and the real tiles replace them without changing the height. A good CLS is 0.1 or less (web.dev); further causes are covered in the article Avoiding Cumulative Layout Shift.

The click also counts toward Interaction to Next Paint. A good INP is 200 milliseconds or less (web.dev). The click handler therefore only sets the loading state and the placeholders and then yields the main thread. Fetching, inserting and tracking follow afterward. The insertion itself happens in one step, not tile by tile, and new tiles set up their scripts only when they are needed. How to break up long tasks is shown in the article Optimizing INP. The following example sums up the steps; the endpoint /listing/chunk stands for a route that delivers only the tiles of one section.

load-more.html
<!-- Without script, a plain link to the next section -->
<ul class="product-grid" id="product-list" aria-busy="false">
  <!-- Tiles of section 1, each with fixed image dimensions -->
</ul>
<p class="listing-status" id="listing-status" aria-live="polite"></p>
<a class="load-more" href="?p=2" data-next-page="2">Load more products</a>

<script type="module">
  const link = document.querySelector('.load-more');
  const list = document.getElementById('product-list');
  const status = document.getElementById('listing-status');

  // Placeholders with the same height as real tiles (via CSS)
  function addSkeletonTiles(count) {
    const tiles = Array.from({ length: count }, () => {
      const li = document.createElement('li');
      li.className = 'product-tile is-skeleton';
      return li;
    });
    list.append(...tiles);
    return tiles;
  }

  link.addEventListener('click', async (event) => {
    event.preventDefault();
    const page = Number(link.dataset.nextPage);

    // 1. Respond right away: loading state and placeholders
    list.setAttribute('aria-busy', 'true');
    const skeletons = addSkeletonTiles(24);

    // 2. Yield to the main thread, then fetch
    await globalThis.scheduler?.yield?.();
    const response = await fetch(`/listing/chunk?p=${page}`);
    const html = await response.text();

    // 3. Insert in one step and remove the placeholders
    const fragment = document.createRange().createContextualFragment(html);
    const firstNew = fragment.firstElementChild;
    skeletons[0].before(fragment);
    skeletons.forEach((tile) => tile.remove());
    list.setAttribute('aria-busy', 'false');

    // 4. Update the address, move focus to the first new tile
    history.replaceState({ page }, '', `?p=${page}`);
    firstNew?.querySelector('a')?.focus();
    status.textContent = 'More products loaded';
    link.dataset.nextPage = String(page + 1);
    link.href = `?p=${page + 1}`;
  });
</script>

Two details decide whether the button also works with a keyboard and screen reader. After loading, focus moves to the first new tile; otherwise it stays on a button that has moved further down, and the new products sit unnoticed before it. And a status line with aria-live announces that new products are there. The button itself can state how many products are still to come, which Google lists as an advantage of this pattern (Google Search Central). With every click, however, the page tree grows. How to limit that is explained in the article Reducing DOM size; a simple limit is to switch to page numbers after a fixed number of sections.

Infinite Scroll: Convenient, but Costly in the Details

Infinite scroll removes the click, and that is exactly what affects measurement. Scrolling and hovering do not count as interactions for INP (web.dev). For CLS, scrolling is not an input that excuses layout shifts: the flag for recent input only applies to discrete events such as tap, click or key press, not to scrolling, dragging or pinch zoom (web.dev). If a newly loaded section pushes the footer or a row down while someone scrolls, every shift counts in full.

Two rules follow from this. First, the next section loads early enough to be ready before anyone sees the end of the list. An IntersectionObserver on an element at the end of the list does this when its rootMargin extends the observed area downward; without a value, observation starts only at the edge of the viewport (MDN Web Docs). Second, the space for the next section is already there before the data arrives: tile-sized placeholders that the real tiles replace without changing the height.

The Nielsen Norman Group names further weaknesses: the footer with contact information and return policies becomes unreachable, the list lacks landmarks, and without a loading indicator users get the impression that they have reached the end (Nielsen Norman Group 2022). For keyboard and screen reader users, the feed pattern of the W3C authoring practices describes how such a list stays usable: Control + End moves focus past the feed, and while articles are being added, the container carries aria-busy="true" (W3C WAI-ARIA Authoring Practices). A newer variant shows page numbers between the newly loaded sections. The Nielsen Norman Group sees them as landmarks that make it easier to refind items; the footer, however, still stays out of reach (Nielsen Norman Group 2022).

Infinite scroll, too, needs its own persistent address for every section. Google recommends absolute page numbers such as ?page=12 instead of relative values such as ?date=yesterday and advises updating the displayed address through the History API as soon as a newly loaded section becomes the primary visible one (Google Search Central). replaceState fits this better than pushState: otherwise every section creates its own history entry, and the back button steps backward section by section instead of returning to the previous page.

Returning to the Same Spot

Few paths in a shop are as common as list, product, back. According to Chrome usage data, 1 in 10 navigations on desktop and 1 in 5 on mobile are a back or forward navigation (web.dev). Landing at the top of the list on the way back is costly: anyone who has to find their position in a long list again loses time, and some visitors give up at this point. On mobile devices, this weighs more heavily because fewer tiles fit on one screen.

The fastest way back is the back/forward cache. It keeps the entire page, including the JavaScript heap, in memory and restores it on back navigation (web.dev), with all appended sections and the scroll position. This applies equally to all three patterns. The prerequisite is that the category page is allowed into the cache: no unload handlers, no unnecessary Cache-Control: no-store, no open connections when leaving (web.dev). How to check and establish eligibility is covered in the article on the back/forward cache.

If the cache does not apply, the browser reloads the page, and that is where an address per section proves its worth. With pagination, ?p=3 delivers the same page as before, and with history.scrollRestoration set to auto, the browser restores the scroll position itself (MDN Web Docs), provided the tiles have fixed dimensions and the height is already right at restore time. With "Load more" and infinite scroll, this basis is missing if the address stays unchanged. The address should therefore follow along on every "Load more" click, for example with history.pushState, so that the right part of the list can be loaded on the way back. The code example above uses replaceState: it keeps the address in sync just the same, but does not create a separate history entry per section.

A robust pattern for the way back without the cache looks like this: the address carries the section in which the clicked tile sits, and history.state remembers the product. On the way back, the server renders only this section, a "Load previous products" link sits above it, and a small script scrolls to the remembered tile. If you set the position yourself, turn off automatic restoration with history.scrollRestoration = "manual" (MDN Web Docs), so browser and script do not work against each other. The benefit for initial load: the way back loads one section instead of all the sections that were visible before.

One URL per Section

An address per section helps with more than going back. It can be reloaded, shared and bookmarked, and it shows the same products on every visit as long as the page number is absolute. Filters and sort order belong in it as separate parameters, so that ?p=3 with an active filter is something different from ?p=3 without one; how filters stay fast in the process is covered in the article on filters in the shop listing. For the cache, this has a side effect: every combination of page, filter and sort order is a separate entry, and deep pages are requested so rarely that they are often not in the cache.

For search engines, one paragraph is enough, because the rules are clear. Google crawls addresses from the href of links, asks for a unique URL per page, for example with ?page=n, and explicitly advises against using the first page of a series as the canonical for all others: every page gets its own canonical link. Page numbers do not belong after a #, because Google ignores the part after the hash, and Google no longer uses rel="next" and rel="prev". Variants with filters or a different sort order should stay out of the index, for example with noindex (Google Search Central). From page 2 onward, Shopware sets the canonical link to the category address with ?p= (Shopware source code).

Deep Pages Strain the Database

The first page and a very deep page look the same, but they do not cost the database the same. The usual query with LIMIT and OFFSET does not skip the rows before it, it reads them. The MariaDB documentation works this through for a list of 50,000 items with ten items per page: for the last page, the database has to find all 50,000 rows, step over the first 49,990 and then deliver ten (MariaDB documentation). If a crawler requests all 5,000 pages, it touches about 125 million items along the way (MariaDB documentation). Depending on filtering and sorting, even the first page may require reading the whole list (MariaDB documentation).

In Shopware, counting comes on top. For the listing, Shopware requests the exact total number of hits (Shopware source code), because the pagination needs the last page number. The source code labels this mode as slow. Besides it, there is a mode that only checks whether further pages follow: it fetches at most six times the page size plus one row (Shopware source code). For "Load more" and infinite scroll, this information is enough, because the button only needs to know whether there is more. The MariaDB documentation describes the same idea for custom queries: query LIMIT 11 instead of LIMIT 10 and use the eleventh item to tell whether there is a next page (MariaDB documentation).

The basic recommendation of the MariaDB documentation is to avoid OFFSET and remember where the last page left off (MariaDB documentation). This fits "Load more" well, because the next section starts right after the last product shown anyway. Search servers have the same limit in a different form: by default, Elasticsearch pages through at most 10,000 hits with from and size, and for deeper results the documentation points to search_after (Elastic documentation). For a deep page, each shard has to load the hits of all previous pages into memory (Elastic documentation).

For running a shop, this means: serve deep pages from the HTTP cache where possible, limit the pagination to a window around the current page, as the Shopware theme does, and watch requests with high page numbers in the slow query log and the access log. How cache layers work together is explained on our page on caching strategies; how to find slow queries and speed them up with indexes is covered in the article on database query optimization.

Which Pattern for Which Listing?

There is no single pattern for all lists. The table sums up what each pattern brings along by default and what only comes with extra work.

CriterionPaginationLoad moreInfinite scroll
Initial load at equal section sizeHTML and links, no script requiredplus script for fetching and insertingplus observer, preloading and address updates
Own URL per sectionbuilt in (?p=n)with a link as fallback and the History APIonly with the History API and a page parameter
Return to the same spotvia bfcache or address and scroll restorationvia bfcache or section address with remembered productvia bfcache or section address, otherwise difficult
Layout shifts when loading morehardly any on page changeclick protects for 500 ms, placeholders needed after thatscrolling does not protect, placeholders and early loading needed
Footer reachableincluded included only with a pause or button
Fitslarge categories, search results, B2B listscategories for browsing, especially on mobileinspiration and new-arrival pages

For most category pages, a foundation of real page links is usually the more robust choice, extended by a script into "Load more" where browsing dominates. That way, every section stays addressable, search engines find all products, and the script only improves what already works without it. Infinite scroll fits pages where visitors browse rather than search, and then needs section addresses, early loading, reserved space and a stop after a few sections so the footer stays reachable. Field data decides which template comes first: the category page with plenty of traffic and a weak INP is the first candidate.

Measuring Whether the Pattern Holds Up

Field data and a few targeted tests show whether the chosen pattern stays fast in practice. INP reveals how quickly a click on a page number or on "Load more" gets a response; which code slows it down is made visible by the Long Animation Frames API, described in the article on Long Animation Frames. For the rest, a short list helps:

  • Record layout shifts after loading more with hadRecentInput: false and attribute them to the section that caused them.
  • With infinite scroll, check whether the next section is ready before the end of the list becomes visible, including on a throttled connection.
  • Measure the share of back navigations restored from bfcache through the pageshow event and its persisted property (web.dev).
  • Test the way back by hand: open a product in the third section, go back, once with and once without bfcache.
  • Open every section address on its own: it shows the same products, has its own canonical link and is reachable without script.
  • Look for requests with very high page numbers in the access log and compare their response time with the first page.

In an analysis, we go through exactly these points for your category templates, with lab values for desktop and smartphone and, where available, field data from your category pages. The result is a recommendation for the loading pattern per template and an order of changes by impact. How we proceed is described under technical analysis. For Shopware shops, we implement the changes in the theme, from section size to the way back without the cache; more on the Shopware performance page. An overview of typical slowdowns in online shops is given on the page PageSpeed for e-commerce.

Sources and documentation

This article is based on data from: Google Search Central (Pagination, incremental page loading, and their impact on Google Search; Fix Lazy-Loaded Website Content); web.dev (Cumulative Layout Shift; Interaction to Next Paint; Back/forward cache); Nielsen Norman Group 2022 (Infinite Scrolling: When to Use It, When to Avoid It); HTTP Archive Web Almanac 2025 (Performance chapter); Bitkom 2025 (Black Friday: Die Hälfte geht auf Schnäppchenjagd); MariaDB documentation (Pagination Optimization); Elastic documentation (Paginate search results); Shopware source code, version 6.7.15.1 (listing pagination, pagination template, listing plugin, CMS controller, category canonical, Criteria); MDN Web Docs (History: scrollRestoration; IntersectionObserver: rootMargin); W3C WAI-ARIA Authoring Practices (Feed Pattern).

Related Articles

Front-end optimisation

Product Gallery and Zoom: Fast Product Pages Despite Images

Product gallery and zoom without the speed penalty: first image eager and prioritized, other images lazy, zoom file on click, variant switch without a reload.

18 min read
Shops & CMS

Fast Shop Listing Filters: Facets Without the Wait

Every filter click in a shop counts toward INP. Confirm the click at once, compute facet counts sparingly, cache results and rebuild the listing without shifts.

19 min read
Core Web Vitals & measurement

Soft Navigations in CrUX: What SPA Shops Check by May 2027

Since October 2026, CrUX has shown Core Web Vitals with soft navigations. Who is affected, how LCP, INP and CLS shift and what SPA shops should do by May 2027.

17 min read