In short and in plain language
Many shops have filters. With them, you can find all blue jackets, for example. Each click on a filter should show a quick response.
The first sign matters most. The check mark should show up right away. The new products may come a bit later.
The server should do little work. Numbers next to each filter take time. Show them only where they help.
The page should not jump while it loads. The space for the products stays free until they arrive.
You measure this with real data from visitors. We are happy to help you with this.
A click on “Blue” in the filter sidebar, and at first nothing happens: the check mark appears late, the product tiles flicker, the page slides up. On category pages with many items, the filter is one of the most important tools for turning a long list of results into the right products, and at the same time one of the most expensive interactions in the shop. Every click sets off a chain: the shop recalculates the results and often the counts of the other facets, sends the result back, and the browser rebuilds the grid of product tiles. In the field, exactly this click goes into Interaction to Next Paint, and a value of no more than 200 milliseconds (web.dev) counts as good. Using Shopware as the example, this article shows where the time between click and result list goes, how to confirm the click at once, which facets really need counts, when a search server and a result cache help, how to rebuild the listing without a layout shift and how to measure all of it with long animation frames.
Key takeaways
- Every filter click is an interaction for INP: it is measured from the click to the next painted frame, and no more than 200 milliseconds (web.dev) counts as good. Only 69 percent of mobile secondary pages reach that (HTTP Archive Web Almanac 2025).
- The click handler only sets the check mark and loading state, then frees the main thread. Request, rebuild and tracking run afterwards, and an AbortController cancels an outdated request (MDN Web Docs).
- Facet counts are aggregations. If they follow the selection, every facet needs its own filtered calculation. Show counts only where they make the choice easier; price as a range, availability as a toggle without a number.
- For large catalogs, filters belong on a search server: Shopware calls the Elasticsearch integration worthwhile from several thousand data sets (Shopware documentation). The HTTP cache also answers frequent filter combinations.
- Layout shifts in the first 500 milliseconds after the click can be excluded from CLS (web.dev), later ones count. So the grid keeps its height until the result arrives. Long animation frames show which script slows down the rebuild.
Why the filter click counts toward INP
Interaction to Next Paint observes all clicks, taps and key presses during a visit and in the end reports the longest of them, minus outliers (web.dev). The check mark in a filter sidebar is a click like any other, but more work hangs on it than on an accordion or a menu. What is measured is not the time until the new results arrive, but the time until the browser paints the next frame that responds to the click. An INP of no more than 200 milliseconds (web.dev) counts as good, and above 500 milliseconds (web.dev) the response counts as poor. The assessment uses the 75th percentile of page loads, split between mobile and desktop (web.dev). How the metric is built in general is explained in the article Optimizing INP.
Filters are a typical interaction after loading. According to Chrome usage data, visitors spend 90 percent of their time on a page after it has loaded (web.dev). On a category page, that is the time spent scrolling, filtering and sorting. Whoever filters a lot collects many interactions. For pages with many interactions, INP ignores the slowest one for every 50 interactions (web.dev); most visits, however, do not reach that many interactions. Then the slowest one counts in full, and if that is a filter click, it sets the INP of the visit.
The Web Almanac shows how far apart home pages and secondary pages are. On mobile, 77 percent of websites had a good INP in 2025 (HTTP Archive Web Almanac 2025), on desktop 97 percent (HTTP Archive Web Almanac 2025). At page level, mobile home pages reach 80 percent (HTTP Archive Web Almanac 2025), secondary pages only 69 percent (HTTP Archive Web Almanac 2025). The authors point out that secondary pages often contain more complex interactions such as filters, carousels and form validation (HTTP Archive Web Almanac 2025). Then there is the device: 73 percent of online shoppers aged 16 and over in Germany shop on a smartphone (Bitkom 2025), on hardware that usually needs more time for the same rebuild than a developer machine. How to test on such devices is described in the article Target device, not test device.
Scope
What happens between click and result list
Before anything can be made faster, it has to be clear where the time goes. A filter click in a server-rendered shop typically passes through six stations, and only some of them are in the browser:
- Input: the browser receives the click. If a long script is running, the click waits. That is the input delay.
- Handler: the filter script reads the new state, builds the URL with the filter parameters and writes it to the address bar.
- Request: the shop sends the combination to the server, often with a second request for the facets.
- Server: it filters and sorts the results, loads the result page and, where needed, calculates the aggregations for the facet counts.
- Rebuild: the result replaces the old grid; style, layout and paint run for all new tiles.
- Follow-up: scripts for images, wish lists, quick view or tracking are set up for the new tiles.
INP measures only part of this: the input delay until the handlers start, the processing of all handlers and the presentation delay until the next frame (web.dev). If the handler does not wait for the server response, the interaction ends with the frame that shows the check mark and loading state. The server time then does not fall into the INP of this click, but it does not disappear. When the response arrives, the browser rebuilds the grid in tasks of its own. If the visitor already clicks the next filter during that time, this click waits for the rebuild. web.dev describes exactly this overlap of interactions as a particularly challenging part of optimizing INP (web.dev).
How the Shopware default theme works
The chain can be traced in the source code of the Shopware default theme (Shopware source code, as of October 2026). Every change to a multi-select filter immediately triggers a reload of the listing, without a pause in between. The listing plugin first sets loading classes on the filter panel and the product area, sends the filters to the route /widgets/cms/navigation/{navigationId} and explicitly asks for no facet calculation with the parameter no-aggregations. The response is ready-made HTML that replaces the product area; the plugin then calls the initialization of the storefront plugins for the whole page. The new URL lands in the address bar via history.pushState, and if the top of the listing is above the visible area, the plugin scrolls smoothly back to it.
If the setting “Disable filter options without results” is active, a second request is added: to /widgets/cms/navigation/{navigationId}/filter, with the parameters only-aggregations and reduce-aggregations (Shopware source code). The server then calculates a separate aggregation for each facet, filtered by the current selection, and the sidebar disables options without results. Two things are missing by default: a running request is not canceled on the next click, and the responses are inserted in the order in which they arrive. Clicking three colors in quick succession triggers three listing requests, six with the setting enabled. If an older response arrives after a newer one, the plugin inserts it as well.
This is not a bug in the strict sense, because with a fast server response it hardly shows. With large categories, complex filters or weak smartphones, however, exactly these points add up. The following sections tackle them in the order in which they determine the wait.
Confirm the click right away
The most important rule is the simplest: the click is visibly confirmed in the next frame, no matter how long the result takes. Up to about 0.1 seconds (Nielsen Norman Group 1993), a response feels immediate, and up to about 1.0 seconds (Nielsen Norman Group 1993), the flow of thought stays intact, even if the delay is noticed. This rule of thumb has been common for decades and fits the definition of INP: for complex interactions, web.dev recommends giving initial visual feedback quickly, and the next frame is the earliest opportunity to do so (web.dev).
For the filter, that means: set the check mark, mark the option as active, show a subtle loading state over the grid and then free the main thread. Everything else runs afterwards: write the URL, start the request, send the tracking event. web.dev describes a pattern for this that works in all browsers: move non-urgent work into a setTimeout inside a requestAnimationFrame, so it only starts after the next frame (web.dev). A more modern way is scheduler.yield(), described in the article Break up long tasks. The following skeleton shows the order for a custom filter script:
let controller = null;
function onFilterChange(input) {
// Visible right away: check mark and loading state
input.closest('.filter-option').classList.toggle('is-active', input.checked);
listing.classList.add('is-loading');
// Everything else only after the next frame
requestAnimationFrame(() => {
setTimeout(() => loadResults(buildFilterUrl()), 0);
});
}
async function loadResults(url) {
controller?.abort(); // cancel the outdated request
const own = new AbortController();
controller = own;
try {
const response = await fetch(url, { signal: own.signal });
renderTiles(await response.text()); // replace only the tiles
history.pushState(null, '', url);
} catch (error) {
if (error.name !== 'AbortError') throw error;
} finally {
if (controller === own) listing.classList.remove('is-loading');
}
}An AbortController lets you abort one or more web requests whenever needed (MDN Web Docs), here the outdated request from the previous click. This way, no older response can overwrite a newer result, and the main thread does not process responses that nobody wants to see anymore. web.dev explicitly names canceling outgoing requests as a remedy for overlapping interactions (web.dev). With multi-select filters, a short debounce of the request can help as well; the check mark itself is not delayed.
A test question for every filter script
Compute facet counts sparingly
The number next to a filter option is an aggregation: the server counts how many results in the current list carry this property. For a single facet, that is cheap. It gets expensive when the counts are supposed to follow the selection. Whoever picks “Blue” expects the “Size” facet to show only the sizes available in blue. Then every facet needs its own filtered calculation, and the “Color” facet must not include its own selection, otherwise the alternatives disappear. Search servers solve this with filters at the aggregation level and a post filter that narrows the hits while preserving all aggregation options (OpenSearch documentation). In Shopware, the parameter reduce-aggregations takes care of this (Shopware source code).
| Facet | Counts needed? | Cheaper form |
|---|---|---|
| Color, material | Helpful with many values | Counts only for the visible values, more only when expanded |
| Size | Rarely, availability is what matters | Gray out unavailable sizes instead of counting |
| Brand | Helpful for long lists | Sort values by frequency, counts optional |
| Price | not included | Range from minimum and maximum, one single calculation |
| Availability, shipping | not included | Toggle without a number |
| Rating | Rarely | Levels without counts |
The rule of thumb: a count has to make a decision easier. If it only shows that there are results, a state is enough, available or grayed out. Price and availability need no counts at all. And not every facet has to be calculated when the page is first built: collapsed facets can load their values when opened, if the visitor actually needs them.
In Shopware, the lever is the setting “Disable filter options without results”. It improves usability because empty combinations cannot be selected in the first place, but it costs the second request with all filtered aggregations on every click. Whether that pays off is shown by measurement: if the filter request takes longer than the listing request itself, it is the first candidate, whether for an index on the search server, for the cache or for fewer facets with counts. Switching the setting off is a decision about usability and should be agreed with the shop team.
A search server instead of the database for large catalogs
Filtering, sorting and counting across many properties is the work search servers are built for. Shopware puts it cautiously in its setup guide: from several thousand data sets, the Elasticsearch integration is worthwhile (Shopware documentation). The page on performance tweaks is clearer. For systems with large product assortments, Elasticsearch or OpenSearch is described as a must-have, because the database does not cope well above a certain assortment size (Shopware documentation).
Two notes from the same documentation directly affect the listing. First, SHOPWARE_ES_THROW_EXCEPTION=1 should be set when running with a search server. Otherwise, the shop falls back to the database when the search server fails, and the documentation warns that in large projects this can completely overload the database (Shopware documentation). Second, since Shopware 6.7.12.0, listings can load a reduced set of product data (Shopware documentation), for example a teaser of no more than 512 characters without HTML instead of the full description (Shopware documentation). New installations have the setting enabled, existing shops have to switch it on themselves and first check whether the theme and extensions need further product data in the tiles.
The search server speeds up the server side. It only changes INP if the browser used to wait for the response or if visitors click faster than the result arrives. Where the server time of a listing request goes, whether into search, the database or rendering the template, is something we clarify in the technical analysis.
A result cache per filter combination
Many visitors filter the same category in the same way: size M, color black, sorted by price. These responses do not have to be recalculated every time. In Shopware, the listing route and the filter route are enabled for the HTTP cache (Shopware source code); each combination results in its own URL and thus its own cache entry. The documentation names one limitation: for logged-in customers and as soon as products are in the cart, Shopware by default no longer delivers complete pages from the cache (Shopware documentation). In B2B shops, where almost everyone shops logged in, you should therefore expect a large share of filter clicks to miss the cache, and the path without the cache has to be fast enough on its own.
The number of possible combinations is huge, though. A worked example: four facets with eight values each, freely combinable, result in 2 to the power of 32 and thus almost 4.3 billion combinations, without sort order and page number. Only a tiny fraction of that can be warmed up. The cache therefore mainly helps with the frequent combinations, and those are in the access logs: the most used filter URLs of the last few weeks, per category. How a warm-up list is built by urgency is shown in the article Sale launch at midnight. For the cache to hit, four rules apply:
- Same combination, same URL: write parameters and values in a fixed order and leave out empty parameters. Otherwise “Blue, Red” and “Red, Blue” end up in two entries.
- Include page and sort order deliberately: they belong in the key because they change the result. Campaign and tracking parameters do not.
- Tie purging to price and stock: if a price or stock level changes, the affected listing entries have to go, including in a cache that sits in front of the shop.
- Mind the context: customer group, currency and language change the result and thus the key. How individual cookies split up the hit rate is described in the article One cookie wrecks your cache.
Setting up the HTTP cache, a reverse proxy in front of it and invalidation is part of our caching strategies. For the listing, one number counts in the end: the share of filter requests served from the cache, split between logged-in and anonymous visitors.
Rebuild the listing without a layout shift
After the click, content moves on many category pages: the grid shrinks because there are fewer results, the result count gets wider, a notice bar appears. For Cumulative Layout Shift, what matters is when this happens. Layout shifts within 500 milliseconds of an input carry a flag and can be excluded from the calculation (web.dev). If the result arrives later, which easily happens with a slow filter request, the shift counts in full. A CLS of no more than 0.1 (web.dev) counts as good.
For exactly this case, web.dev recommends creating space right away and showing a loading indicator when an interaction triggers a request that may take a while (web.dev). For the listing, that means:
- The grid keeps its height while loading; the old tiles stay in place and are dimmed instead of disappearing.
- The image areas of the tiles have a fixed aspect ratio, so images that load later do not move anything.
- Result count, active filters and sort order sit in areas with a fixed height.
- An empty result gets a notice the height of one row of tiles instead of an empty area.
- Notices about the changed selection appear in an existing line, not as a new bar above the grid.
Further causes and ways to measure them are described in the article Avoid Cumulative Layout Shift. The second lever is the amount that gets repainted. If HTML is inserted via JavaScript, the browser only frees the main thread again once it has parsed and rendered that HTML (web.dev). With small amounts, this is unproblematic, with large ones it delays the next frame (web.dev) and with it the next filter click.
The fewer tiles per page and the flatter the markup of a tile, the shorter the rebuild. Tiles below the visible area can be excluded from rendering with content-visibility, as described in the article content-visibility; background on node counts is in the article Reduce DOM size. And if all plugins on the page are set up again after every rebuild, it is worth checking which of them the new tiles really need.
Measure with long animation frames
Whether a filter is fast enough is decided by field data, not by the impression on a developer machine. The Long Animation Frames API reports every frame whose rendering update is delayed by more than 50 milliseconds (Chrome for Developers) and names the scripts involved with source and invoker, provided a script ran longer than 5 milliseconds (Chrome for Developers). The API has been available in Chrome since version 123 (Chrome for Developers). Long tasks start at the same threshold: anything that occupies the main thread for more than 50 milliseconds at a stretch counts as a long task (web.dev).
The open-source web-vitals library links these frames to the measured interaction. In the attribution build, it returns for INP the element of the interaction (interactionTarget), the three sub-parts (inputDelay, processingDuration, presentationDelay), the intersecting long animation frames and the longest script together with the phase in which it ran (web-vitals library). This makes it possible to group INP by element: all interactions whose target lies in the filter sidebar form a group of their own, separate from cart, navigation and gallery. The basics are covered in the article Long animation frames. A measurement plan for the category template:
- Evaluate field data for category pages separately from home and product pages, mobile and desktop separately, each at the 75th percentile.
- Group the INP interactions by
interactionTargetand pull out the filter sidebar. - For each filter click, find the largest sub-part: a long input delay points to third-party scripts or a running rebuild, long processing to heavy handlers, a long presentation delay to too much new markup.
- From the long animation frames, sort the scripts with the longest duration by source: theme, extension or third party.
- In the lab, record the same click sequence with a throttled CPU in Chrome DevTools and measure the server time of the listing and the filter request alongside.
Because the filter click changes the URL via pushState and rebuilds the content, it may also play a role in measuring soft navigations in the future. What shops with page changes without a reload should check is covered in the article Soft navigations in CrUX.
The rule for every filter
Order of implementation
Not every measure costs the same. A sensible order starts with small changes in the theme and only then moves on to the server and infrastructure:
- Measure: INP of the category template in the field, filter clicks pulled out via attribution, plus the server time of both requests.
- Confirm right away: check mark and loading state in the first frame, request and tracking after it, cancel outdated requests.
- Shrink the rebuild: replace only the tiles, hold the space, set up plugins only for new elements.
- Review facets: counts only where they help with the choice, and measure the filter request on its own.
- Relieve the server: a search server for large catalogs, reduced product data in the listing, a cache for frequent combinations.
- Safeguard: a budget for the INP of the category page and a click sequence in the lab test that runs with every theme change.
The first three steps are in the theme and can usually be implemented without touching the database. Step five is an infrastructure decision and pays off mainly where measurement shows that the server time outweighs the rebuild. We support the implementation in the Shopware theme, search and cache as part of Shopware performance. The page Core Web Vitals brings together the basics of INP, LCP and CLS, frontend optimization is the starting point for work on scripts and markup, and for shops on other systems the same chain applies, as described on the page PageSpeed for online shops.
Sources and documentation
Related Articles
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.
Sale Launch at Midnight: Warm the Cache Instead of Waiting
At sale launch, prices change and the cache goes cold. Build a warm-up list from field data and the sitemap, plan the night and measure hits before release.
What Does PageSpeed Optimization Cost? Planning the Effort
PageSpeed optimization costs depend on templates, shop system, scripts, server and images. How to plan analysis, fixed price, monitoring and a 2027 budget.