Dowiedz się, jak zbudować szybkie listy w panelu z 100k wierszy: paginacja, wirtualizacja, mądre filtrowanie i lepsze zapytania, by narzędzia wewnętrzne pozostały responsywne.

Widok listy zwykle działa płynnie, dopóki nagle nie zaczyna zwalniać. Użytkownicy zauważają drobne przestoje: skokowe przewijanie, chwilowe zawieszanie strony po każdej aktualizacji, filtry reagujące z opóźnieniem i spinner po każdym kliknięciu. Czasem karta przeglądarki wygląda na zablokowaną, bo wątek UI jest zajęty.
100k wierszy to częsty punkt zwrotny, bo obciąża wszystkie części systemu jednocześnie. Zbiór danych wciąż jest normalny dla bazy, ale na tyle duży, że drobne nieefektywności stają się widoczne w przeglądarce i w sieci. Próba pokazania wszystkiego naraz zamienia prosty ekran w ciężki potok.
Celem nie jest renderowanie wszystkich wierszy. Celem jest pomóc komuś szybko znaleźć to, czego potrzebuje: właściwe 50 wierszy, następna strona albo wąski wycinek po filtrze.
Warto podzielić pracę na cztery części:
Jeśli któraś z tych części jest kosztowna, cały ekran będzie odczuwalnie wolny. Proste pole wyszukiwania może wyzwolić zapytanie, które sortuje 100k wierszy, zwraca tysiące rekordów, a potem zmusza przeglądarkę do ich wyrenderowania. Tak właśnie pisanie staje się opóźnione.
Gdy zespoły szybko tworzą narzędzia wewnętrzne (w tym na platformach typu vibe-coding jak Koder.ai), ekrany list często są pierwszym miejscem, gdzie rzeczywisty wzrost danych ujawnia różnicę między "działa na zestawie demo" a "jest natychmiastowe codziennie".
Zanim zaczniesz optymalizować, zdecyduj, co oznacza „szybko” dla tego ekranu. Wiele zespołów goni przez przepustowość (załadować wszystko), podczas gdy użytkownicy potrzebują głównie niskiej latencji (szybka reakcja). Lista może wydawać się natychmiastowa, nawet jeśli nigdy nie wczytuje wszystkich 100k wierszy, pod warunkiem że szybko reaguje na przewijanie, sortowanie i filtry.
Praktyczny cel to czas do pierwszego wiersza, a nie czas pełnego załadowania. Użytkownicy ufają stronie, gdy widzą szybko pierwsze 20–50 wierszy i interakcje pozostają płynne.
Wybierz mały zestaw liczb, które możesz śledzić przy każdej zmianie:
COUNT(*) i szerokie SELECTy)To mapuje się na typowe symptomy. Jeśli CPU przeglądarki skacze przy przewijaniu, frontend robi za dużo pracy na wiersz. Jeśli spinner czeka, ale przewijanie potem jest OK, zwykle problem jest w backendzie lub sieci. Jeśli zapytanie jest szybkie, a strona nadal się zamraża, prawie zawsze to renderowanie lub ciężkie przetwarzanie po stronie klienta.
Zrób prosty eksperyment: zostaw UI bez zmian, ale tymczasowo ogranicz backend, aby zwracał tylko 20 wierszy przy tych samych filtrach. Jeśli zrobi się szybko — wąskie gardło to rozmiar ładunku lub czas zapytania. Jeśli dalej jest wolno — sprawdź renderowanie, formatowanie i komponenty per-wiersz.
Przykład: ekran Zamówienia działa wolno podczas wpisywania w wyszukiwarce. Jeśli API zwraca 5 000 wierszy i przeglądarka filtruje je przy każdym naciśnięciu klawisza, pisanie będzie opóźnione. Jeśli API zajmuje 2 sekundy przez COUNT na nieindeksowanym polu, zobaczysz czekanie zanim pojawi się jakikolwiek wiersz. Różne naprawy, ta sama skarga użytkownika.
Przeglądarka często jest pierwszym wąskim gardłem. Lista może działać wolno mimo szybkiego API, bo strona próbuje namalować zbyt wiele. Pierwsza zasada: nie renderuj tysięcy wierszy w DOM naraz.
Jeszcze przed pełną wirtualizacją utrzymuj każdy wiersz lekkim. Wiersz z wieloma zagnieżdżeniami, ikonami, tooltipami i złożonymi stylami kosztuje przy każdym przewinięciu i każdej aktualizacji. Preferuj zwykły tekst, kilka małych odznak i tylko jedną–dwie interaktywne rzeczy na wiersz.
Stała wysokość wiersza pomaga bardziej, niż się wydaje. Gdy każdy wiersz ma tę samą wysokość, przeglądarka lepiej przewiduje układ i przewijanie jest płynne. Wiersze o zmiennej wysokości (zawijające opisy, rozszerzalne notatki, duże awatary) powodują dodatkowe mierzenia i reflow. Jeśli potrzebujesz dodatkowych szczegółów, rozważ panel boczny lub pojedynczy obszar rozwijalny zamiast pełnego wielowierszowego wiersza.
Formatowanie to kolejny ukryty koszt. Daty, waluty i ciężkie operacje na stringach sumują się, gdy powtarzasz je w wielu komórkach.
Jeśli wartość nie jest widoczna, nie obliczaj jej jeszcze. Cachuj kosztowne formatowania i obliczaj je na żądanie, np. gdy wiersz stanie się widoczny lub gdy użytkownik go otworzy.
Szybki zestaw poprawek, który często daje zauważalny zysk:
Przykład: tabela Faktury formatująca 12 kolumn walut i dat będzie klatkować przy przewijaniu. Cachowanie sformatowanych wartości na fakturę i opóźnianie pracy dla wierszy poza ekranem może sprawić, że wszystko będzie natychmiastowe, jeszcze zanim zaczniesz optymalizować backend.
Wirtualizacja oznacza, że tabela rysuje tylko te wiersze, które są faktycznie widoczne (plus mały bufor nad i pod). Przy przewijaniu ponownie używa tych samych elementów DOM i zamienia w nich dane. Dzięki temu przeglądarka nie maluje dziesiątek tysięcy komponentów wierszy naraz.
Wirtualizacja dobrze sprawdza się przy długich listach, szerokich tabelach lub "ciężkich" wierszach (awatarach, chipach statusu, menu akcji, tooltipach). Przydaje się też, gdy użytkownicy dużo przewijają i oczekują płynnego ciągłego widoku, zamiast skakania strona po stronie.
To nie magia. Kilka rzeczy często zaskakuje:
Najprostsze podejście jest nudne: stała wysokość wiersza, przewidywalne kolumny i niezbyt dużo widgetów interaktywnych w wierszu.
Możesz połączyć obie techniki: użyj paginacji (lub cursor-based load more), aby ograniczyć, co pobierasz z serwera, i wirtualizacji, aby utrzymać tani rendering w obrębie pobranego wycinka.
Praktyczny wzorzec: pobieraj normalny rozmiar strony (często 100–500 wierszy), wirtualizuj w tej stronie i daj jasne kontrolki zmiany stron. Jeśli używasz infinite scroll, dodaj widoczny indicator "Załadowano X z Y", żeby użytkownicy wiedzieli, że nie widzą jeszcze wszystkiego.
Jeśli chcesz listy, która pozostaje użyteczna wraz ze wzrostem danych, paginacja jest zwykle najbezpieczniejszym domyślnym wyborem. Jest przewidywalna, dobrze działa w przepływach administracyjnych (przeglądanie, edycja, akceptacja) i wspiera potrzeby typu eksport "strona 3 z tymi filtrami" bez niespodzianek. Wiele zespołów wraca do paginacji po próbach bardziej wyrafinowanego przewijania.
Infinite scroll może być przyjemny przy swobodnym przeglądaniu, ale ma ukryte koszty. Ludzie tracą poczucie miejsca, przycisk wstecz często nie przywraca do tej samej pozycji, a długie sesje mogą narastać w pamięci, gdy ładujesz kolejne wiersze. Środkowy kierunek to przycisk Load more, który wciąż używa stron, więc użytkownicy pozostają zorientowani.
Offset (page=10&size=50) to klasyczne podejście. Jest proste, ale może zwalniać na dużych tabelach, bo baza może musieć pominąć wiele wierszy, aby dotrzeć do późniejszych stron. Może też dziwnie wyglądać, gdy pojawiają się nowe wiersze i elementy przesuwają się między stronami.
Keyset (zwany też cursor pagination) prosi o "następne 50 wierszy po ostatnim widzianym", zwykle używając id lub created_at. Zwykle pozostaje szybki, bo nie musi tyle pominąć.
Praktyczna reguła:
Użytkownicy lubią widzieć totaly, ale pełne "policz wszystkie pasujące wiersze" może być kosztowne przy ciężkich filtrach. Opcje to cache'owanie countów dla popularnych filtrów, uaktualnianie licznika w tle po załadowaniu strony lub pokazanie przybliżonego wyniku (np. "10 000+").
Przykład: ekran Zamówienia może pokazywać wyniki natychmiast z paginacją keyset, a dokładny total wypełnić dopiero gdy użytkownik przestanie zmieniać filtry przez sekundę.
Jeśli budujesz to w Koder.ai, traktuj paginację i zachowanie licznika jako element specyfikacji ekranu wcześnie, żeby wygenerowane zapytania backendu i stan UI nie walczyły później.
Większość ekranów list wydaje się wolna, bo zaczynają szeroko otwarte: ładuj wszystko, a potem poproś użytkownika, żeby zawęził. Odwróć to. Zacznij od sensownych domyślnych ustawień, które zwracają mały, użyteczny zbiór (np. Ostatnie 7 dni, Moje elementy, Status: Otwarte), i zrób opcję Wszystkie czasy świadomym wyborem.
Wyszukiwanie tekstowe to kolejna pułapka. Jeśli wykonujesz zapytanie przy każdym naciśnięciu klawisza, tworzysz kolejkę żądań i UI, które migocze. Debounce wejście wyszukiwania, aby zapytać dopiero po krótkiej pauzie użytkownika, i anuluj starsze żądania przy nowym. Prosta zasada: jeśli użytkownik nadal pisze, nie uderzaj w serwer.
Filtry działają szybko tylko wtedy, gdy są czytelne. Pokaż chipy filtrów przy górze tabeli, aby użytkownicy widzieli, co jest aktywne i mogli to usunąć jednym kliknięciem. Trzymaj etykiety chipów po ludzku, nie jako surowe nazwy pól (np. Właściciel: Sam zamiast owner_id=42). Gdy ktoś mówi "moje wyniki zniknęły", zwykle chodzi o niewidoczny filtr.
Wzorce, które utrzymują dużą listę responsywną bez komplikowania UI:
Zapisane widoki to cichy bohater. Zamiast uczyć użytkowników tworzenia idealnej jednorazowej kombinacji filtrów, daj im kilka presetów dopasowanych do realnych zadań. Zespół ops może przełączać się między "Nieudane płatności dziś" a "Klienci o wysokiej wartości" — to jeden klik i łatwiej utrzymać takie widoki szybkie po stronie backendu.
Jeśli budujesz narzędzie wewnętrzne w builderze sterowanym czatem jak Koder.ai, traktuj filtry jako część przepływu produktu, nie dodatek. Zacznij od najczęstszych pytań, zaprojektuj widok domyślny i zapisane widoki wokół nich.
Widok listy rzadko potrzebuje tych samych danych, co strona szczegółów. Jeśli API zwraca wszystko o wszystkim, płacisz podwójnie: baza robi więcej, a przeglądarka otrzymuje i renderuje więcej niż potrzebuje. Kształtowanie zapytań to nawyk proszenia tylko o to, czego lista potrzebuje teraz.
Zacznij od zwracania tylko kolumn potrzebnych do renderowania wiersza. Dla większości pulpitów to id, kilka etykiet, status, właściciel i znaczniki czasu. Duże teksty, bloby JSON i pola obliczeniowe mogą poczekać, aż użytkownik otworzy wiersz.
Unikaj ciężkich joinów dla pierwszego paintu. Joiny są ok, gdy trafiają w indeksy i zwracają małe wyniki, ale stają się kosztowne, gdy łączysz wiele tabel i potem sortujesz lub filtrujesz po dołączonych danych. Prostym wzorem jest: szybko pobierz listę z jednej tabeli, a potem ładuj szczegóły na żądanie (albo batchowo dla widocznych wierszy).
Ogranicz opcje sortowania i sortuj po indeksowanych kolumnach. "Sortuj po wszystkim" brzmi pomocnie, ale często zmusza do wolnych sortów na dużych zbiorach. Preferuj kilka przewidywalnych opcji jak created_at, updated_at czy status i upewnij się, że te kolumny są zindeksowane.
Uważaj na agregacje po stronie serwera. COUNT(*) na ogromnym przefiltrowanym zbiorze, DISTINCT na szerokiej kolumnie czy obliczanie całkowitej liczby stron mogą zdominować czas odpowiedzi.
Praktyczne podejście:
COUNT i DISTINCT jako opcjonalne; cache'uj lub przybliżaj, jeśli to możliweJeśli budujesz narzędzia wewnętrzne na Koder.ai, zdefiniuj lekkie zapytanie listowe oddzielnie od zapytania szczegółowego już w fazie planowania, aby UI pozostał responsywny wraz ze wzrostem danych.
Jeśli chcesz, by widok listy pozostał szybki przy 100k wierszy, baza danych musi robić mniej pracy na żądanie. Większość wolnych list to nie "za dużo danych", tylko zły wzorzec dostępu do danych.
Zacznij od indeksów dopasowanych do rzeczywistych potrzeb użytkowników. Jeśli lista jest zwykle filtrowana po status i sortowana po created_at, potrzebujesz indeksu wspierającego oba te pola, w tej kolejności. W przeciwnym razie baza może przeskanować znacznie więcej wierszy i potem je sortować, co szybko kosztuje.
Naprawy, które zwykle przynoszą największe korzyści:
tenant_id, status, created_at).OFFSET. OFFSET zmusza bazę do przechodzenia wielu wierszy, żeby je pominąć.Prosty przykład: tabela Zamówienia pokazująca nazwę klienta, status, kwotę i datę. Nie dołączaj wszystkich powiązanych tabel i nie pobieraj pełnych notatek zamówienia do widoku listy. Zwróć tylko kolumny używane w tabeli i załaduj resztę w osobnym żądaniu po kliknięciu zamówienia.
Jeśli budujesz z platformą typu Koder.ai, pamiętaj o tym podejściu nawet gdy UI jest generowany z czatu. Upewnij się, że generowane endpointy API wspierają paginację kursorową i selektywne pola, aby praca bazy pozostała przewidywalna wraz ze wzrostem tabeli.
Jeśli strona listy działa wolno, nie zaczynaj od przepisywania wszystkiego. Najpierw zdefiniuj, jak wygląda normalne użycie, a potem optymalizuj tę ścieżkę.
Zdefiniuj widok domyślny. Wybierz domyślne filtry, kolejność sortowania i widoczne kolumny. Listy zwalniają, gdy próbują pokazać wszystko domyślnie.
Wybierz styl paginacji pasujący do użycia. Jeśli użytkownicy głównie przeglądają pierwsze strony, klasyczna paginacja wystarczy. Jeśli użytkownicy skaczą głęboko (strona 200+) lub potrzebujesz stabilnej wydajności niezależnie od odległości, użyj keyset pagination (na przykład created_at + id).
Dodaj wirtualizację do części tabeli. Nawet jeśli backend jest szybki, przeglądarka może się dławić przy renderowaniu zbyt wielu wierszy.
Spraw, by wyszukiwanie i filtry wydawały się natychmiastowe. Debounce podczas pisania, by nie wysyłać zapytań przy każdym naciśnięciu. Trzymaj stan filtrów w URL lub w jednym współdzielonym store, aby odświeżenie, przycisk wstecz i udostępnianie widoku działały poprawnie. Cache'uj ostatni udany wynik, żeby tabela nie migała pusta.
Mierz, potem dopracowuj zapytania i indeksy. Loguj czas serwera, czas bazy, rozmiar payloadu i czas renderu. Potem przytnij zapytanie: wybierz tylko kolumny, które pokazujesz, stosuj filtry jak najwcześniej i dodaj indeksy dopasowane do domyślnego filtr + sort.
Przykład: dashboard wsparcia z 100k zgłoszeń. Domyślnie Otwarte, przypisane do mojego zespołu, sortowane po najnowszych, pokazujące sześć kolumn i pobierające tylko id, temat, przypisaną osobę, status i znaczniki czasu. Z keyset pagination i wirtualizacją zarówno baza, jak i UI pozostają przewidywalne.
Jeśli budujesz narzędzia w Koder.ai, ten plan dobrze pasuje do iteracyjnego workflow: zmień widok, przetestuj przewijanie i wyszukiwanie, a potem dopracuj zapytanie, aż strona będzie responsywna.
Najszybszy sposób zepsuć listę to traktować 100k wierszy jak zwykłą stronę danych. Większość wolnych dashboardów ma kilka przewidywalnych pułapek.
Jedną z głównych jest renderowanie wszystkiego i ukrywanie tego za pomocą CSS. Nawet jeśli wygląda na to, że widocznych jest tylko 50 wierszy, przeglądarka i tak płaci za stworzenie 100k węzłów DOM, ich mierzenie i repaint przy przewijaniu. Jeśli potrzebujesz długich list, renderuj tylko to, co użytkownik może zobaczyć (wirtualizacja) i upraszczaj komponenty wierszy.
Wyszukiwanie może też cicho zniszczyć wydajność, gdy każdy naciśnięty klawisz wyzwala pełne skanowanie tabeli. Dzieje się tak, gdy filtry nie mają indeksów, gdy przeszukujesz zbyt wiele kolumn lub gdy wykonujesz zapytania typu contains na ogromnych polach tekstowych bez planu. Dobra zasada: pierwszy filtr, do którego sięga użytkownik, powinien być tani w bazie, nie tylko wygodny w UI.
Kolejny problem to pobieranie pełnych rekordów, gdy lista potrzebuje tylko podsumowań. Wiersz listy zazwyczaj potrzebuje 5–12 pól, nie całego obiektu, nie długich opisów i nie powiązanych danych. Pobieranie dodatkowych danych zwiększa pracę bazy, czas sieci i parsowanie po stronie klienta.
Eksporty i totaly mogą zamrozić UI, jeśli obliczasz je na głównym wątku lub czekasz na ciężkie zapytanie przed reakcją. Utrzymuj UI interaktywny: uruchamiaj eksporty w tle, pokazuj postęp i unikaj przeliczania totalów przy każdej zmianie filtra.
Na koniec: zbyt wiele opcji sortowania może się zemścić. Jeśli użytkownicy mogą sortować po dowolnej kolumnie, będziesz sortować duże zestawy w pamięci lub zmusisz bazę do wolnych planów. Trzymaj sortowania do małego zestawu indeksowanych kolumn i dopasuj domyślne sortowanie do istniejącego indeksu.
Szybki test orientacyjny:
Traktuj wydajność listy jak cechę produktu, nie jednorazową poprawkę. Lista jest szybka tylko wtedy, gdy rzeczywiści ludzie mogą płynnie przewijać, filtrować i sortować na realnych danych.
Użyj tej checklisty, by potwierdzić, że naprawiłeś właściwe rzeczy:
Prosta kontrola rzeczywistości: otwórz listę, przewijaj przez 10 sekund, a potem zastosuj powszechny filtr (np. Status: Otwarte). Jeśli UI zamrozi się, problem zwykle leży w renderowaniu (za dużo DOM) lub w ciężkiej transformacji po stronie klienta (sortowanie, grupowanie, formatowanie) wykonywanej przy każdej aktualizacji.
Kolejne kroki, w tej kolejności, żeby nie skakać między poprawkami:
Jeśli budujesz to z Koder.ai (koder.ai), zacznij w Planning Mode: zdefiniuj dokładne kolumny listy, pola filtrów i kształt odpowiedzi najpierw. Potem iteruj, używając snapshotów i rollbacku, gdy eksperyment spowolni ekran.
Zmień cel z „załaduj wszystko” na „pokaż szybko pierwsze użyteczne wiersze”. Optymalizuj czas do pierwszego wiersza i płynność interakcji przy filtrowaniu, sortowaniu i przewijaniu, nawet jeśli nigdy nie ładujesz całego zbioru naraz.
Mierz czas do pierwszego wiersza po załadowaniu lub zmianie filtra, czas aktualizacji po filtrowaniu/sortowaniu, rozmiar odpowiedzi (payload), wolne zapytania bazodanowe (szczególnie szerokie SELECTy i COUNT(*)) oraz skoki na głównym wątku przeglądarki. Te liczby bezpośrednio odpowiadają odczuwalnym opóźnieniom.
Tymczasowo ogranicz API do zwracania tylko 20 wierszy przy tych samych filtrach i sortowaniu. Jeśli robi się szybko — problem leży w kosztach zapytania lub rozmiarze payloadu; jeśli dalej jest wolno — zlokalizuj problem po stronie renderowania, formatowania lub pracy wykonywanej na każdy wiersz.
Nie renderuj tysięcy węzłów DOM naraz, upraszczaj komponenty wierszy i stosuj stałą wysokość wiersza. Unikaj ciężkiego formatowania dla niewidocznych wierszy — obliczaj i cachuj formatowanie tylko, gdy wiersz stanie się widoczny lub gdy użytkownik go otworzy.
Wirtualizacja montuje tylko widoczne wiersze (plus mały bufor) i ponownie używa elementów DOM przy przewijaniu. Ma sens przy długich listach lub «ciężkich» wierszach, ale działa najlepiej, gdy wysokość wiersza jest spójna, a układ tabeli przewidywalny.
Paginacja jest zwykle najbezpieczniejszym wyborem dla przepływów administracyjnych — utrzymuje orientację użytkownika i ogranicza pracę serwera. Infinite scroll sprawdza się przy swobodnym przeglądaniu, ale ma ukryte koszty: gubienie kontekstu, problemy z przyciskiem wstecz i rosnące użycie pamięci, chyba że wprowadzisz jasne ograniczenia.
Offset (np. page=10&size=50) jest prosty, ale może zwalniać przy głębokich stronach, bo baza musi pominąć wiele wierszy. Keyset (cursor) paginacja pobiera „następne 50 po ostatnim widzianym” i zwykle utrzymuje stałą wydajność, ale gorzej nadaje się do skakania na konkretną stronę.
Nie wysyłaj zapytań przy każdym naciśnięciu klawisza. Debounce wejścia tekstowego, anuluj trwające zapytania przy nowych żądaniach i domyślnie stosuj zawężające filtry (np. ostatnie 7 dni, Moje elementy), aby pierwsze zapytanie było małe i użyteczne.
API listy powinno zwracać tylko pola potrzebne do renderowania wiersza — zwykle id, etykietę, status, właściciela i znaczniki czasu. Duże teksty, bloby JSON i dane powiązane przenieś do żądania szczegółowego, aby pierwszy widok pozostał lekki i przewidywalny.
Dopasuj indeksy do rzeczywistych filtrowań i sortowań użytkowników. Dodaj indeksy złożone odzwierciedlające typowe filtry + sort (np. tenant_id, status, created_at). Traktuj dokładne liczniki jako opcjonalne — cache'uj je, preobliczaj lub pokazuj przybliżone wartości, żeby nie blokowały głównej odpowiedzi.