Common Infinite Scroll Bugs and How to Catch Them
Common Infinite Scroll Bugs and How to Catch Them are critical to address before releasing any modern web or mobile application. Infinite scroll has become a default pattern for feeds, product listing
Common Infinite Scroll Bugs and How to Catch Them are critical to address before releasing any modern web or mobile application. Infinite scroll has become a default pattern for feeds, product listings, and timelines, yet its seemingly simple implementation hides a variety of subtle defects that only surface under real‑world usage. This guide walks through the most common infinite‑scroll bug patterns, explains why each occurs, shows how they appear to users, provides reproducible steps, and offers concrete fixes and preventive measures. You’ll also find a test matrix, a symptom‑to‑fix table, and a short checklist you can bookmark for daily work.
Why Infinite Scroll Is Prone to Bugs
Infinite scroll relies on a tight coupling between UI interaction (scroll), data fetching (often asynchronous), and state management (what has been rendered, what is pending, and when to stop). Each of these layers introduces failure modes:
- Timing assumptions – code often assumes a network response arrives before the user scrolls further. Variability in latency breaks that assumption.
- State synchronization – the UI must know whether a request is in flight, whether data is new or duplicate, and whether the end of the list has been reached. Missed updates cause duplicates or stale UI.
- Edge‑case handling – empty responses, error payloads, or sudden changes in item height can break scroll‑position calculations.
- Performance pressure – as the list grows, DOM nodes accumulate, event listeners multiply, and memory usage climbs if old nodes aren’t recycled.
- Accessibility and focus – dynamic insertion can shift focus or announce incorrect screen‑reader states, especially when users rely on keyboard or assistive tech.
Because scripted tests usually follow a fixed scroll amount or a pre‑recorded sequence, they miss the variability introduced by real user personas (e by different network speeds, device capabilities, and interaction rhythms. Persona‑driven autonomous exploration, which simulates curious, impatient, novice, and other user types, is far more likely to trigger these hidden defects.
Below are ten bug patterns that appear repeatedly in production. Each pattern includes a symptom description, root cause, reproduction steps, detection tactics (manual and automated), and a fix/prevention guide.
Bug Pattern 1: Missing Sentinel Leads to Endless Loading Spinner
Symptom
After scrolling to the bottom of the list, a spinner appears and never disappears, even though no more data exists on the server.
Root Cause
The component checks for a “hasMore” flag returned by the API. When the final page returns an empty array, the flag is either omitted or incorrectly set to true. The UI therefore keeps issuing requests, each returning an empty payload, and the spinner stays visible.
How to Reproduce
- Open the feed in a browser or emulator.
- Scroll down until the network panel shows a request with
page=5(or whatever the last page is) returning[]and"hasMore": true(or missing). - Continue scrolling; observe the spinner persists indefinitely.
Detection Strategies
- Manual – watch the network tab while scrolling to the end; note any request that returns zero items but still triggers another request.
- Automated unit test – mock the API to return an empty array with
hasMore: trueon the Nth call and assert that the component stops requesting after a configurable threshold (e.g., two consecutive empty responses). - Automated UI test – use a scrolling loop in Playwright or Appium that stops after a set number of scrolls and asserts that the spinner element is not present after a timeout.
Fix & Prevention
- Ensure the API contract explicitly includes a boolean
hasMorethat isfalsewhen the response array is empty. - In the client, treat any empty response as a signal to stop, regardless of the flag, and log a warning if the flag contradicts the payload.
- Add a safeguard: after N consecutive empty responses (where N=2 is typical), cease further requests and show an “You’re all caught up” message.
Bug Pattern 2: Duplicate Items Appear After Rapid Scroll
Symptom
When the user flicks the scroll bar quickly, the same item appears twice in a row, breaking the visual flow.
Root Cause
The scroll‑position listener fires multiple times before the previous request completes. Each handler calculates the same offset and triggers a new fetch, appending the same page of data again. Debouncing or throttling is missing or set too loosely.
How to Reproduce
- Open a list with a known page size (e.g., 20 items per request).
- Perform a fast swipe or drag that moves the viewport past the threshold twice within 300 ms.
- Inspect the rendered list; you’ll see the last items of the first page duplicated at the start of the second page.
Detection Strategies
- Manual – use a touchpad or mouse to flick quickly; watch for visual repeats.
- Automated – simulate two scroll events spaced 100 ms apart using
page.mouse.wheel(0, 300)(Playwright) ordriver.swipe(Appium). After the second event, assert that the DOM does not contain duplicate IDs or keys from the previous batch. - Unit test – spy on the fetch function; ensure it is called only once per scroll‑threshold crossing when events are spaced less than the debounce window.
Fix & Prevention
- Implement a debounce (e.g., 200 ms) or throttle on the scroll handler so that only the last scroll position within the window triggers a fetch.
- Track a “loading” flag; ignore further scroll events while a request is pending.
- Assign stable keys to each rendered item (based on a server‑provided ID) and, in development, run a rule that flags duplicate keys in the DOM.
Bug Pattern 3: Scroll Position Jumps After New Data Inserts
Symptom
After new items load, the viewport jumps upward, causing the user to lose their place and sometimes see the same content again.
Root Cause
The component updates the list by prepending new items to the top of the DOM (common in chat‑style feeds) or by replacing the entire list without preserving the scroll offset. The browser’s scroll position is measured from the top, so inserting nodes above the current view shifts everything down, making the visual position appear to move up.
How to Reproduce
- Scroll halfway down a long list.
- Wait for a background refresh that prepends new items (e.g., pull‑to‑refresh at the top).
- Observe that the visible items shift; the previously centered item is now higher on screen.
Detection Strategies
- Manual – note the distance a known element moves after a refresh; use dev tools to measure its
offsetTopbefore and after. - Automated – capture the
scrollTopof the container before triggering a refresh, then after the update assert thatscrollTophas increased by approximately the height of the newly inserted nodes (or that the visual position of a tracked element stays within a tolerance). - Visual regression – take a screenshot of the viewport before and after the refresh; compare using a pixel‑diff tool with a small threshold.
Fix & Prevention
- When inserting at the top, adjust the container’s
scrollTopby the total height of the inserted nodes so the visual position remains stable. - Alternatively, render new items at the bottom and keep the top‑only insertion for cases where the UI explicitly wants to show newest first (e.g., chat). In that case, maintain scroll position by anchoring to the bottom.
- Use CSS
transform: translateY()on a wrapper to offset the jump without causing layout thrashing.
Bug Pattern 4: Accessibility Trap – Focus Lost or Announced Incorrectly
Symptom
Keyboard users navigating with Tab or arrow keys suddenly lose focus, or screen readers announce “Loading…” repeatedly, making the feed confusing.
Root Cause
Dynamic insertion of elements can reset the tab order or cause the browser to blur the active element when the container’s innerHTML is replaced. Additionally, ARIA live regions may be misconfigured, causing excessive announcements.
How to Reproduce
- Navigate the list using only the keyboard (Tab to enter, then Arrow Down to move through items).
- Trigger a scroll that loads more items.
- Observe whether focus jumps to the top of the page or to an unrelated element, and listen to screen‑reader output.
Detection Strategies
- Manual – use a screen reader (NVDA, VoiceOver) and keyboard only; note any loss of focus or spurious announcements.
- Automated – in Playwright, focus an item (
await item.focus()), then trigger a scroll that loads data, and assertawait page.isFocused(item)remains true or that the newly focused item is the next logical element. - Unit test – verify that any DOM mutation uses
appendChildorinsertBeforerather thaninnerHTML = ""when preserving focus, and that ARIAaria-liveis set topolite(notofforassertivefor routine updates).
Fix & Prevention
- Preserve focus by storing the currently focused element before updating the list and restoring it afterward (
focus()). - If the list is rebuilt, assign
tabindex="-1"to the container and callfocus()on it after the update, then move focus to the item that previously had focus. - Configure ARIA live regions appropriately: use
aria-live="polite"for updates that are not critical, and ensure the region exists from the initial render (do not add/remove it on each load).
Bug Pattern 5: Memory Leak from Unbounded DOM Growth
Symptom
After extended use, the page becomes sluggish, memory usage climbs steadily, and eventually the browser tab may crash or show an “out of memory” warning.
Root Cause
Each scroll event appends new nodes to the list but never removes nodes that have scrolled far out of view. Over hundreds of items, the DOM balloons, causing expensive layout and paint cycles.
How to Reproduce
- Open the infinite‑scroll page in Chrome DevTools → Performance → Memory.
- Scroll continuously for two minutes, forcing dozens of page loads.
- Observe the JS heap size increasing without plateau.
Detection Strategies
- Manual – open the Memory tab, take a heap snapshot before and after a long scroll session, compare retained size.
- Automated – in a test script, scroll a fixed number of times (e.g., 50), then force a garbage collection (
window.gc()if available) and assert that the number of DOM nodes does not exceed a predefined limit (e.g., initial nodes + 2× page size). - Lint rule – enforce that the virtualization library (e.g., react‑window, vue‑virtual-scroller) is used for lists > 100 items, or that a cleanup function removes nodes whose
offsetTopis far outside the viewport.
Fix & Prevention
- Adopt virtual scrolling: render only the subset of items that are within the viewport plus a small buffer.
- If virtualization is not feasible, implement a manual cleanup: after each append, measure the distance of the first visible item; remove nodes that are more than, say, three viewport heights above the top.
- Use CSS
contain: stricton list items to limit the browser’s need to recompute styles for off‑screen elements. - Monitor memory in CI with a Puppeteer script that scrolls and checks
performance.memory.
Bug Pattern 6: Race Condition Causes Missing Items
Symptom
Occasionally, after scrolling fast, a gap appears in the list where one or more items are completely absent, showing only placeholder skeletons or blank space.
Root Cause
Two requests for consecutive pages are launched nearly simultaneously. The slower request resolves after‑responding request overwrites the DOM with its data, discarding the items that arrived first. Without request deduplication or sequencing, the UI ends up showing only the later batch.
How to Reproduce
- Throttle the network to simulate uneven latency (e.g., 200 ms for page N, 800 ms for page N+1).
- Scroll quickly so that both requests are triggered before either resolves.
- Wait for both to finish; examine the list for missing IDs from the first page.
Detection Strategies
- Manual – use Chrome DevTools Network throttling; watch the order of responses and the rendered list.
- Automated – mock the API to delay responses differently for each page; after triggering two rapid scrolls, assert that the concatenated list of rendered item IDs matches the expected sequence (no gaps, no duplicates).
- Unit test – ensure the data‑layer queues requests by page number and only processes the latest response for a given page, discarding out‑of‑order responses.
Fix & Prevention
- Attach a monotonically increasing request ID or page number to each fetch; upon response, ignore data if its ID is less than the currently loaded highest page.
- Use a promise‑per‑page map: store the promise for each page; subsequent calls for the same page return the existing promise instead of launching a duplicate.
- Serialize scroll‑triggered loads with a simple lock: set
isLoading = trueat start, reset tofalseon settle, and prevent new loads whileisLoadingis true unless the user has scrolled past a new threshold.
Bug Pattern 7: Incorrect Threshold Causes Premature Fetch
Symptom
The spinner appears while the user is still comfortably reading the current items, leading to unnecessary network traffic and a jarring experience.
Root Cause
The code compares scrollTop + clientHeight to scrollHeight - threshold. If threshold is set too large (e.g., 800 px on a mobile screen), the condition fires well before the actual bottom is reached.
How to Reproduce
- Open the page with dev tools showing the scroll values.
- Slowly scroll down and note the values at which the spinner appears.
- If the spinner shows when
scrollTop + clientHeightis still far fromscrollHeight, the threshold is too high.
Detection Strategies
- Manual – log
scrollTop,clientHeight, andscrollHeighton each scroll event; verify that the trigger condition only fires when the remaining distance is less than a reasonable buffer (e.g., 100‑200 px). - Automated – in a test, scroll to a position where the remaining distance is 500 px, then assert that no request is launched.
- CI lint – enforce a maximum threshold value (e.g., 250 px) in the codebase via an ESLint rule.
Fix & Prevention
- Compute the threshold dynamically based on viewport height:
threshold = Math.max(100, viewportHeight * 0.2). - Clamp the threshold to a sensible maximum (e.g., 200 px) to avoid premature loads on large screens.
- Provide a dev‑only overlay that shows the trigger point as a colored bar, making it easy to spot mis‑calculations during QA.
Bug Pattern 8: Failed Request Handling Leads to Blank Screen
Symptom
When a network error occurs (timeout, 500, or malformed JSON), the list stops rendering new items and shows only a blank area or the last loaded items, with no indication to retry.
Root Cause
The catch block for the fetch promise either swallows the error or sets the loading state to false without updating an error state, leaving the UI in a limbo where it thinks it’s done loading.
How to Reproduce
- Use a network throttling profile that forces a 504 error on a specific page request.
- Scroll until that page is fetched.
- Observe that the spinner disappears and no further items appear, even after scrolling further.
Detection Strategies
- Manual – open the Console to see error logs; check that an error message or retry button appears.
- Automated – mock the API to reject with a network error; after the scroll triggers the request, assert that an error indicator element is present and that a retry action (button click) restores loading.
- Unit test – verify that the reducer or state machine transitions to an
errorstate on rejected promises and that the view renders an appropriate message.
Fix & Prevention
- Always capture errors in fetch chains and set an
errorflag in UI state. - Render an error banner with a retry button; clicking it should re‑issue the failed request.
- Implement exponential back‑off and a maximum retry count to avoid hammering a failing endpoint.
- Log errors to a monitoring service (e.g., Sentry) for post‑release analysis.
Bug Pattern 9: Infinite Loop Due to Mis‑calculated Offset
Symptom
The page keeps requesting new data nonstop, causing the network tab to flood with requests and the browser to become unresponsive.
Root Cause
The offset or page number used for the next request is not incremented correctly (e.g., stays at 0 or is set to currentOffset + pageSize - 1). Consequently, each request asks for the same slice of data, and the hasMore flag remains true, creating a tight loop.
How to Reproduce
- Add a breakpoint or log in the request‑building function to show the offset/page.
- Scroll to trigger a load; note that the offset does not change after the first load.
- Watch the network panel for repeated identical requests.
Detection Strategies
- Manual – log the request parameters; verify monotonic increase.
- Automated – after each successful request, assert that the next request’s offset is at least
previousOffset + pageSize. - Test with mocked slow API – introduce a 2‑second delay; if the loop is present, you’ll see the number of requests grow without bound within a short time.
Fix & Prevention
- Encapsulate pagination logic in a pure function:
nextOffset = currentOffset + pageSize. - Write unit tests for this function covering edge cases (first page, last page, zero‑size page).
- Use a request‑queue system that rejects duplicate offsets.
Bug Pattern 10: Ads or Interstitials Breaking Scroll Logic
Symptom
After an ad loads and expands, the scroll position jumps unexpectedly, or the infinite‑scroll trigger fires multiple times in quick succession, causing duplicate loads.
Root Cause
Ads that inject iframes or dynamically resize containers change the scrollHeight of the scrolling element without notifying the scroll listener. The listener may then calculate a false proximity to the bottom, triggering extra loads. Additionally, ad click‑throughs can shift focus or cause the user to scroll unintentionally.
How to Reproduce
- Load a page known to serve interstitial ads after every Nth item.
- Scroll until an ad appears; note any sudden jump in the scrollbar or the appearance of a loading spinner.
- Disable the ad blocker and repeat; compare behavior with ads enabled vs disabled.
Detection Strategies
- Manual – use dev tools to disable JavaScript for ad networks and observe if the jump disappears.
- Automated – mock ad iframe insertion that changes container height by a known amount; after the change, assert that no additional load is triggered unless the user has actually scrolled past the threshold.
- Accessibility check – ensure that ad containers are inert (
aria-hidden="true") when off‑screen and that they do not trap focus.
Fix & Prevention
- Decouple the scrolling container from ad containers: place ads in a separate overlay or in a fixed‑size slot that does not affect the main list’s
scrollHeight. - Use
ResizeObserveron the main list to detect genuine size changes due to new items, ignoring changes from known ad slots. - Provide a fallback threshold that is based on the distance to the actual list end, computed by subtracting the sum of known fixed‑height elements (headers, ads, footers) from
scrollHeight.
Test Matrix: Manual vs Automated Detection Approaches
| Bug Pattern | Manual Detection Steps | Automated Detection (Unit) | Automated Detection (UI) | Typical Tools |
|---|---|---|---|---|
| 1 – Missing sentinel | Watch network for zero‑item responses that still trigger a request | Mock API to return {data:[], hasMore:true} and assert request count stops after N empties | Playwright: scroll to bottom, wait for spinner, assert it disappears after timeout | Jest, MSW, Playwright |
| 2 – Duplicate items | Fast flick, visually inspect for repeats | Spy on fetch; ensure it’s called once per scroll‑threshold crossing within debounce window | Appium: two rapid swipes, assert no duplicate IDs in DOM | Jest, Appium |
| 3 – Jumping scroll position | Note element’s offset before/after refresh | Capture scrollTop before update, assert after update it compensates for inserted height | Cypress: scroll mid‑list, trigger refresh, assert tracked element stays within viewport | Cypress, Jest |
| 4 – Accessibility trap | Keyboard + screen‑reader navigation, listen for announcements | Focus an item, trigger load, assert focus retained or moved logically | Playwright with axe-core: check for focus loss and live‑region misuse | axe-core, NVDA, Playwright |
| 5 – Memory leak | Take heap snapshots before/after long scroll | Scroll N times, force GC, assert DOM node count < limit | Puppeteer: loop scroll, measure performance.memory after each iteration | Chrome DevTools, Puppeteer |
| 6 – Race condition | Throttle network, scroll fast, spot missing IDs | Mock delayed responses, assert rendered IDs match expected sequence without gaps | Playwright: two overlapping scrolls, validate list continuity | MSW, Playwright |
| 7 – Premature fetch | Log scroll values, note early spinner | Assert no request fires when remaining distance > threshold buffer | Jest: mock scroll event, verify fetch not called | Jest |
| 8 – Failed request handling | Force 504, see if retry UI appears | Mock rejected promise, assert error state and retry button | Cypress: intercept 500, click retry, assert reload | Cypress, MSW |
| 9 – Infinite loop | Log offset/page, watch for repeats | Unit test pagination function for monotonic increase | Playwright: count requests in a time window, assert < limit | Jest, Playwright |
| 10 – Ads breaking scroll | Scroll with ad blocker off/on, compare jumps | Mock ad height change, assert no extra load unless true threshold crossed | Playwright: insert ResizeObserver mock, verify no spurious fetch | Playwright, Jest |
The table shows that most bugs benefit from a combination of unit tests (to validate pure logic) and UI‑level tests (to catch timing and rendering issues). Manual exploratory testing remains valuable for spotting UX‑specific quirks like focus loss or visual jumps.
Using Persona‑Driven Autonomous Exploration to Surface Hidden Scroll Bugs
Scripted test suites often follow a predetermined scroll distance or a fixed number of iterations, which can miss edge cases that only appear when real users interact with the feed in varied ways. Autonomous QA platforms such as SUSA simulate distinct user personas—each with its own behavior profile—to stress the infinite‑scroll implementation more thoroughly.
- Curious persona – lingers on items, reads descriptions, and scrolls slowly, often pausing to open modals. This profile tends to trigger bugs related to premature fetch thresholds and race conditions because the timing between scroll events is irregular.
- Impatient persona – flicks the scroll bar aggressively, trying to reach the bottom as fast as possible. It is excellent at uncovering duplicate‑item issues, premature loads caused by low debounce delays, and memory‑leak symptoms from rapid DOM growth.
- Novice persona – relies heavily on visual cues and may miss subtle indicators like a missing spinner. It often reveals accessibility problems such as focus loss or poor error messaging.
- Adversarial persona – attempts to break the app by performing unexpected sequences: scrolling up and down rapidly, triggering orientation changes, or disabling network mid‑scroll. This profile surfaces infinite‑loop bugs, race conditions, and faulty error handling.
- Elderly / accessibility persona – uses larger text, screen readers, and keyboard navigation exclusively. It catches ARIA live‑region misuse, focus traps, and insufficient contrast in loading indicators.
- Power‑user persona – employs keyboard shortcuts, middle‑click or right‑click to open links in new tabs, and expects the scroll position to persist across actions. It highlights bugs where scroll position is not preserved after dynamic inserts or after ad interactions.
By letting an autonomous agent explore the app with these profiles, QA teams can discover bugs that would require a large matrix of manual test cases to cover. The agent builds a knowledge base of visited screens and dead ends; each subsequent run becomes smarter because it avoids re‑testing already‑verified paths and focuses on unexplored edges.
When integrating SUSA into a CI pipeline, you can:
- Upload the latest APK or provide the staging URL.
- Choose the set of personas you want to exercise (the platform defaults to all eight).
- Define acceptance criteria—for example, “no spinner should remain visible for more than 2 seconds after the final page loads.”
- Let the agent run; it will generate a report that lists any infinite‑scroll anomalies, complete with steps to reproduce, screenshots, and network traces.
- Optionally, export the discovered flows as Appium (Android) or Playwright (Web) regression scripts so that future runs can verify the fixes automatically.
Because the exploration is driven by real‑world behavior patterns rather than pre‑written scripts, it catches the subtle timing, concurrency, and UI‑state bugs that often slip past traditional test suites.
Quick Checklist for Developers and QA
| ✅ Item | Description |
|---|---|
| Sentinel correctness | API must return a reliable hasMore flag; client treats empty data as stop condition. |
| Debounce / throttle | Scroll handler should be debounced (≈150‑200 ms) or throttled to prevent duplicate fetches. |
| Loading state guard | Do not start a new request while isLoading is true unless a new threshold is crossed. |
| Focus preservation | Store active element before list updates and restore it (or move focus logically) after render. |
| Virtualization or cleanup | For lists > 100 items, use virtual scrolling or periodically remove off‑screen DOM nodes. |
| Error handling | Catch fetch errors, show an error UI with retry, and avoid silent failures. |
| Threshold calculation | Base trigger distance on viewport height (e.g., 20 % of clientHeight) with a reasonable max (≈200 px). |
| ARIA live region | Use aria-live="polite" for updates; ensure the region exists from the initial render. |
| Ad slot isolation | Keep ads in containers that do not affect the main list’s scroll height or use ResizeObserver to filter out ad‑driven size changes. |
| Test coverage | Unit test pagination logic, debounce, and error states; UI test scroll‑triggered loads, focus, and memory growth. |
| Persona‑driven checks | Run autonomous exploration with at least three distinct personas (curious, impatient, accessibility) each release cycle. |
Apply this checklist during feature development, code review, and pre‑release verification to keep infinite‑scroll bugs at bay.
Closing Takeaways
Infinite scroll looks simple, but it intertwines UI interaction, asynchronous data, and state management in ways that create a surprising variety of defects. The ten patterns covered here—missing sentinel, duplicates, jumpy scroll, accessibility focus loss, memory growth, race conditions, premature thresholds, poor error handling, offset loops, and ad‑induced interference—represent the most frequent sources of user‑visible problems in production.
Detecting these issues requires a blend of tactics: unit tests that validate pure functions (pagination, debounce), UI‑level tests that assert behavior after scroll actions, manual exploratory checks for focus and visual jitter, and persona‑driven autonomous exploration that mimics real‑world usage patterns. Tools like Playwright, Appium, Jest, MSW, axe‑core, and platforms such as SUSA make it feasible to automate many of these checks while still留出 space for human intuition.
Fixing the bugs is usually straightforward once the root cause is identified: ensure reliable pagination flags, guard against duplicate requests with loading states, preserve focus and scroll position, limit DOM growth, handle errors gracefully, compute thresholds based on viewport size, and isolate any third‑party content that can alter layout. By adopting the checklist and integrating automated scroll‑specific tests into your CI, you can turn infinite scroll from a source of hidden regressions into a reliable, performant, and user‑friendly pattern.
Keep this guide bookmarked, run the matrix regularly, and let autonomous exploration handle the combinatorial complexity—your users will thank you with smoother feeds and fewer frustrating surprises.
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free