Probeer 14 dagen gratis
Skip to content

Dynamische productfilters in WooCommerce: tellingen zonder SQL per klik

Een filter dat “Blauw (14)” toont en dat getal niet bijwerkt, liegt. Zodra de bezoeker maat 42 kiest, hoort blauw alleen nog de sneakers te tellen die én blauw én maat 42 zijn. Opties die dan op nul vallen, horen uit de lijst te verdwijnen. Dat heet dynamisch filteren. De meeste WooCommerce-plugins doen die hertelling met een SQL-COUNT per optie, via admin-ajax.php. InstantFilter doet dezelfde hertelling in de browser, op de producten van het huidige archief, zonder die queries.

Wat zijn dynamische productfilters in WooCommerce?

Een statisch filter is een lijst die bij het laden van de categorie is vastgezet. Maat 36 staat erop, ook als er in “Heren > Sneakers” geen enkele maat 36 ligt. De bezoeker klikt, krijgt een leeg grid, en haakt af. Een dynamisch filter herberekent na elke keuze twee dingen: hoeveel producten elke resterende optie nog oplevert, en of die optie nog getoond mag worden.

Die twee dingen zijn niet hetzelfde. De teller is het getal tussen haakjes, bijvoorbeeld (14), in het element .if-filter__count. Het verbergen is de volgende stap: een optie met teller 0 gaat op display: none, tenzij de bezoeker die optie zelf aan heeft staan. Een heel filterblok (kleur, maat, merk) verdwijnt pas als er geen enkele zichtbare optie meer in zit én er in dat blok niets gekozen is. Zo blijft een actieve keuze altijd uit te zetten.

Dit speelt op het categorie-archief, niet in de header-zoekbalk. Zoeken en filteren zijn twee taken; die afbakening staat in de gids zoeken vs filteren op WooCommerce-categoriearchieven. Dynamische tellingen horen bij het filteren: de verzameling staat al vast, en elke klik maakt de doorsnede kleiner.

Waarom kosten facet-tellingen bij AJAX zoveel SQL?

Een naïeve implementatie doet per optie een aparte telling. Twintig maten, vijftien kleuren en tien merken zijn vijfenveertig SELECT COUNT(*)-queries, plus de query die het productgrid zelf ophaalt. Elke query join’t wp_postmeta of wp_term_relationships, start WordPress op via admin-ajax.php, en bezet een PHP-worker. Dat is de 800ms tot 2,5 seconden die bezoekers als spinner zien.

Het wordt duurder naarmate de keuze specifieker wordt. De eerste klik (“blauw”) moet voor élke andere facet opnieuw tellen welke maten en merken nog blauwe producten hebben. De tweede klik (“maat 42”) doet die ronde nog eens. Plugins als JetSmartFilters lossen dat op de server op, ook als ze een index hebben: de index scheelt joins, maar de roundtrip en de worker blijven.

Caching helpt hier slecht. Elke combinatie van facetten is een andere query. Een categorie met een handvol attributen heeft meer combinaties dan een page-cache kan onthouden. De archiefpagina zelf is wél cachebaar als de hertelling niet naar de server gaat. Hoe dat samenwerkt met WP Rocket en Redis staat in de gids over filtercaching.

Hoe telt InstantFilter opties zonder de database te vragen?

Na hydratatie staat het archief in de browser: contextItems, de producten van deze categorie, tag of merk. Een klik filtert die set in het geheugen. De functie updateFilterCountsFromItems loopt daarna de facetblokken na en vult de tellers. Dat is dezelfde doorgang als het grid zelf, geen tweede verzoek. Interacties landen in 1,5ms tot 5ms. Tijdens die klik gaat er geen SQL naar MySQL.

Er zijn twee manieren om een optie aan producten te koppelen, afhankelijk van het codebook:

  • Inverted index. Als filtering_mode niet op fx staat en het codebook een inverted_index voor die eigenschap heeft, haalt de engine de item-id’s van die optie uit de index en telt hoeveel daarvan in de huidige set zitten.
  • Eigenschapscodes op het item. Anders vergelijkt de engine de code van de optie met item.props. Bij een hiërarchische taxonomie tellen child-terms mee bij de parent: “Schoenen” telt ook de producten die alleen aan “Sneakers” hangen.

Prijs- en getalfilters slaat deze lus over. Die hebben geen lijst met (14). Hun minimum en maximum schuiven mee met de gefilterde set, via applyRangeBounds. Een prijsschuif die nog €20–€400 toont terwijl de gekozen kleur alleen tussen €80 en €150 ligt, is dus een ander mechanisme dan het verbergen van een lege kleur.

De index op de server bestaat wel, maar niet per klik. if_filter_counts bewaart voorbereide tellingen per eigenschap, waarde en context (globaal, categorie, tag). Die tabel vult de achtergrondindex. De bezoeker die op maat 42 klikt, leest die tabel niet opnieuw uit. Bij catalogi tot tienduizenden SKU’s blijft die hertelling in het geheugen van de browser passen. Boven ongeveer 100.000 SKU’s kan het codebook zelf te groot worden; dan is de afweging een server-index, niet een terugkeer naar een COUNT per optie op de categoriepagina.

Waarom verdwijnt een lege optie, en blijft een gekozen optie staan?

Een optie met teller 0 is een doodlopende klik. InstantFilter zet het label op display: none. De bezoeker ziet “Maat 36” niet meer als er in de huidige doorsnede geen maat 36 is. Dat is het automatische verbergen van lege filters: niet een aparte plugin-instelling met die naam, maar het gedrag van de teller-update zelf.

Er is één uitzondering, en die is bewust. Staat de optie aan (checkbox of radio is checked), dan blijft het label zichtbaar, ook als de teller op 0 zou vallen. Anders verdwijnt de knop waarmee je de keuze ongedaan maakt, en zit de bezoeker vast in een filter die hij niet meer ziet. Hetzelfde geldt voor een optie waarvan de mapping naar het codebook niet betrouwbaar is: een geselecteerde optie blijft staan, een onbekende niet-geselecteerde optie wordt niet zomaar hernoemd.

Voorraad is hier geen aparte verberg-schakelaar. Producten dragen stock_status (instock of outofstock) in de index. Een kleur die alleen nog op uitverkochte items zit, valt op teller 0 zodra die items niet in de gefilterde set zitten, en verdwijnt dan mee. Zolang uitverkochte items wél in de set zitten, telt de kleur mee. De kaart toont de voorraadstatus; de facetlijst volgt de set, niet een verborgen “verberg uitverkocht”-vinkje naast de teller.

Hoe werkt self-exclude, zodat je binnen één facet nog kunt wisselen?

Als de telling de eigen keuze van het facet zou meenemen, zakt elke andere kleur naar 0 zodra je “blauw” aanzet. De lijst zou inklappen tot één optie. InstantFilter telt daarom zoals een groot warenhuis dat doet: voor facet “kleur” telt de engine de producten die aan alle ándere filters voldoen, zonder de kleurkeuze zelf. Maat 42 blijft de set begrenzen. Blauw, zwart en grijs tonen elk hoeveel producten van maat 42 die kleur nog hebben.

In code heet dat self-exclude. getSubsetExcluding kopieert de actieve filters, haalt de sleutel van dít facet eruit, en filtert contextItems met de rest. Facetten waar niets in gekozen is, gebruiken de set die het grid al heeft. Er is een cache op die subset, zodat twee opties in hetzelfde facet niet twee keer dezelfde doorsnede uitrekenen.

Het gevolg voor de bezoeker: hij kan van blauw naar zwart zonder eerst blauw uit te zetten, en hij ziet vóór de klik of zwart nog iets oplevert. Het gevolg voor de server: die subset bestaat alleen in het geheugen. Er is geen tweede query “tel zwart binnen maat 42, maar negeer kleur”.

Een winkel in sneakers maakt dat concreet. Stel dat de categorie “Heren” 240 producten bevat. Zonder keuze toont maat 42 een teller van 60 en blauw een teller van 40. De bezoeker klikt maat 42. De engine filtert die 240 naar die 60. Daarna telt kleur zónder een kleurkeuze en mét de maat: blauw zakt van 40 naar 9, omdat maar 9 van de blauwe paren ook maat 42 zijn. Grijs zakt naar 0 en verdwijnt. Maat 42 zelf blijft staan, want die optie is gekozen.

Daarna klikt hij blauw. Het grid toont 9 producten. De telling voor kleur negeert nu de kleurkeuze en houdt maat 42 vast. Zwart toont hoeveel zwarte paren er in maat 42 nog zijn, ook al is blauw aan. De telling voor maat negeert de maatkeuze en houdt blauw vast, zodat maat 43 laat zien hoeveel blauwe paren die maat heeft. Twee subsets, allebei uit dezelfde 240 items in het geheugen, geen van beide een query.

Zet hij maat 42 uit, dan herberekent dezelfde functie de set op alleen blauw. Maten die bij blauw wél bestaan en bij maat 42 niet, komen terug op het scherm. Het blok was niet uit de pagina verwijderd. Alleen de zichtbaarheid ging aan en uit. Dat is waarom een lege lijst na één klik geen reden is om de filters opnieuw van de server te halen.

Meerdere waarden in hetzelfde facet werken hetzelfde. Vinkt de bezoeker blauw én zwart aan, dan haalt self-exclude de hele sleutel “kleur” uit de telling van dat facet, niet alleen blauw. De andere kleuren worden geteld alsof er nog geen kleur gekozen is, terwijl maat en merk wél blijven gelden. Zo kan hij een derde kleur aanvinken en vóór die klik zien of die nog producten toevoegt. De grid-filter zelf houdt blauw én zwart wél aan: de telling en de resultaten zijn bewust niet dezelfde set.

Dat verschil is de reden dat een teller hoger kan zijn dan het aantal kaarten op het scherm. De kaarten volgen alle gekozen filters, inclusief de kleur die aanstaat. De tellers binnen het kleurblok volgen alle filters behalve kleur. Een bezoeker die dat leest als “de teller liegt” vergelijkt twee vragen. De kaartvraag is “wat houd ik over”. De tellervraag is “wat houd ik over als ik deze kleur loslaat”.

Wanneer verdwijnt een heel filterblok?

updateFacetContainerVisibility kijkt na de opties naar het blok zelf. Elk element .if-filter met een data-prop-key wordt beoordeeld. Prijs en getal slaat deze functie over; die regelen hun zichtbaarheid via de nieuwe min- en maxgrenzen.

Is er in het blok een actieve keuze, dan blijft het blok open. De bezoeker moet die keuze kunnen wissen. Is er geen keuze, en is elke optie verborgen, dan gaat het hele blok op display: none. Komt er later weer een optie bij — omdat de bezoeker een ander filter uitzet — dan zet dezelfde functie het blok terug. Niets wordt uit de DOM gegooid. Het is zichtbaarheid, geen verwijdering, dus de volgende telling kan alles terugzetten.

Dat is het verschil met een klassiek W3-script dat kaarten verbergt en de filterlijst met rust laat. DOM show/hide op productkaarten herberekent geen tellers. Waarom dat op een shop stukloopt, staat in de analyse van de W3-filtertechniek. Dynamische tellingen zijn precies het stuk dat die scripts niet hebben.

Hoe verhouden statische lijsten, AJAX-tellingen en InstantFilter zich?

Statische filterlijstAJAX-telling per optieInstantFilter na hydratatie
Teller na een klikBlijft de beginwaardeOpnieuw via SQLOpnieuw in de browser
Optie met 0 resultatenBlijft klikbaarVaak grijs of weg, na de roundtripMeteen verborgen
Gekozen optieBlijft staanAfhankelijk van de pluginBlijft staan, ook bij 0
Ander kleur naast de gekozen kleurToont de oude tellingCOUNT zonder die kleur, op de serverSelf-exclude in het geheugen
Leeg facetblokBlijft op de paginaWisselendWeg, tot er weer een optie is
SQL per klik0, maar de lijst liegtEén COUNT per optie0
LatencyGeen hertelling800ms – 2.500ms1,5ms – 5ms

Hoe zet je dit op een categorie-archief?

Je bouwt de teller niet zelf in Elementor of Bricks. De page builder is de schil. De hertelling zit in de filterengine, de opmaak van de lijst in de Layout Builder onder InstantFilter → Layouts.

  1. Archief begrenzen. De listing moet de WordPress-context respecteren. Anders telt “blauw” producten van buiten de categorie, en liegt de teller tegen de URL.
  2. Plaatsen. In Bricks een van de vier native elementen, in Elementor het archief-shortcode. Het facetblok is een .if-filter met data-prop-key en data-type.
  3. Eerst de HTML. De categorie wordt server-side gerenderd. Google ziet de producten van dat archief zonder een facet aan te klikken. De tellers worden daarna voor de bezoeker bijgewerkt.
  4. Controleren. Open een categorie met meerdere attributen. Klik één kleur. De maten die daarbij niet voorkomen, verdwijnen. De andere kleuren blijven staan, met een nieuwe teller. Zet de kleur uit: de verborgen maten komen terug. Er gaat geen verzoek naar admin-ajax.php voor die hertelling.

Dynamische tellingen in de browser als

  • De bezoeker facetten combineert: kleur, maat en merk moeten na elke klik een kloppend getal tonen.
  • Lege opties ergeren: een klik die een leeg grid opent, kost conversie. Verbergen bij 0 voorkomt die klik.
  • De categorie groot is: tientallen COUNT-queries per bezoeker schalen niet; het geheugen van de browser wel, tot het codebook te groot wordt.

Niet dit mechanisme verwachten als

  • Je een relevantie-zoekmachine wilt: tellers zijn doorsnedes, geen ranking. De header-zoekbalk blijft een zoekplugin.
  • Je prijs als (n)-lijst wilt: range-filters schuiven hun grenzen, ze tonen geen teller per euro.
  • De catalogus ver boven 100.000 SKU’s zit: dan wordt het codebook voor de browser de beperking, niet de telling zelf.

Veelgestelde vragen over dynamische WooCommerce-filters

Een filter dat na elke keuze de tellers herberekent en opties met nul resultaten verbergt. “Blauw (14)” wordt “Blauw (3)” zodra een andere facet de set kleiner maakt. Een statische lijst laat het oude getal staan.
Omdat je die keuze anders niet meer uit kunt zetten. InstantFilter verbergt opties met teller 0, behalve de optie die op dat moment geselecteerd is. Hetzelfde geldt voor het hele filterblok: zolang er iets in gekozen is, blijft het blok open.
Bij een klassiek AJAX-filter wel: vaak een COUNT per optie, via admin-ajax.php, met joins op wp_postmeta of term-tabellen. Na hydratatie telt InstantFilter in de browser op de producten van het huidige archief. Tijdens die klik gaan er 0 SQL-queries naar de database.
Alleen als die producten niet in de gefilterde set zitten. De index bewaart stock_status per item. Een optie zakt naar 0 en wordt verborgen wanneer geen enkel resterend product die waarde heeft. Er is geen aparte schakelaar die los van de set alle uitverkochte opties verbergt.
De telling van een facet negeert de eigen keuze van dat facet. Maat en merk begrenzen de set wel. Zo zie je hoeveel producten van die maat nog zwart zijn, en kun je van kleur wisselen zonder eerst blauw uit te zetten.

Verdiep je verder in filtertellingen en archieven

Tellers zijn een gevolg van de architectuur. Deze pagina’s gaan door op die architectuur:

Laat facet-tellers meebewegen zonder SQL per klik

Test InstantFilter 14 dagen. Klik een kleur aan en kijk of de maten meteen kloppen, zonder spinner en zonder COUNT-query.

Klaar om filteren instant te maken?

Download de gratis 14-daagse proefversie van Pro. Geen creditcard nodig — direct testen.