Ant Design Select组件onPopupScroll无限滚动性能问题求助
Hey, I've dealt with this exact performance headache when implementing infinite scroll on Ant Design's Select component—let's break down why this is happening and how to fix it step by step.
Why You're Seeing This Lag
Your current setup has a few key performance bottlenecks:
- Frequent re-renders: Every scroll triggers
setState({ loadingState: true }), which forces the entire Select component (and all its Options) to re-render immediately. - No debouncing: Scroll events fire hundreds of times per second—your
loadMorefunction runs way too often, overwhelming React with updates. - Missing unique keys: When mapping over
userList, you don't provide a uniquekeyprop for each Option. React can't reuse existing components, so it recreates every Option on every re-render. - Flawed scroll-end check: Using
===fortarget.scrollTop + target.offsetHeight === target.scrollHeightcan fail due to browser pixel rounding, leading to either missed load triggers or repeated unnecessary calls.
Step-by-Step Fixes
1. Add Unique Keys to Options
This is React 101 for performance—always include a unique, stable key when rendering lists. For your user list, use userList.userNumber since it's unique:
{this.state.userList && this.state.userList.map(user => ( user.userNumber != null && ( <Option key={user.userNumber} value={user.userNumber}> {user.name} </Option> ) ))}
2. Debounce the loadMore Function
Prevent scroll events from triggering loadMore nonstop. Use Lodash's debounce (or write a simple one if you don't want to add Lodash):
First, import debounce (if using Lodash):
import { debounce } from 'lodash';
Then wrap your loadMore function (do this in componentDidMount to avoid recreating it on every render):
componentDidMount() { this.debouncedLoadMore = debounce(this.loadMore, 150); // Adjust delay as needed (100-200ms works well) } componentWillUnmount() { this.debouncedLoadMore.cancel(); // Clean up to avoid memory leaks }
Update your Select's onPopupScroll to use the debounced version:
onPopupScroll={this.debouncedLoadMore}
3. Optimize the Scroll-End Check & Avoid Unnecessary State Updates
Don't set loadingState until you confirm you're at the bottom and aren't already loading. Also, use a more forgiving check for scroll end to account for browser rounding:
loadMore = (e) => { e.persist(); const { target } = e; const { loadingState, currentPage, hasMoreData } = this.state; // Add currentPage/hasMoreData to track pagination // Exit early if already loading or no more data to fetch if (loadingState || !hasMoreData) return; // Check if we're within 10px of the bottom (tolerance for browser pixel rounding) const isNearBottom = target.scrollTop + target.offsetHeight >= target.scrollHeight - 10; if (isNearBottom) { this.setState({ loadingState: true }, () => { // Call your API here with the next page number fetch(`/api/users?page=${currentPage + 1}`) .then(res => res.json()) .then(newUsers => { this.setState(prevState => ({ userList: [...prevState.userList, ...newUsers], currentPage: prevState.currentPage + 1, loadingState: false, hasMoreData: newUsers.length > 0 // Stop loading if no more users are returned })); }) .catch(() => { this.setState({ loadingState: false }); // Reset loading state on error }); }); } };
4. Simplify & Optimize the Filter Option Function
Your current filterOption runs toLowerCase() on every option and input on every keystroke. Simplify it and consider pre-processing user data for faster filtering:
// Simplified filterOption filterOption={(input, option) => { if (!option.props.children) return false; return option.props.children.toLowerCase().includes(input.toLowerCase()); }}
Even better, precompute lowercase names when fetching users to avoid repeated toLowerCase() calls:
// When fetching users, add a lowercaseName property const newUsers = await res.json(); newUsers.forEach(user => { user.lowercaseName = user.name.toLowerCase(); }); // Updated filterOption (faster since we're using precomputed data) filterOption={(input, option) => { const user = this.state.userList.find(u => u.userNumber === option.props.value); return user?.lowercaseName.includes(input.toLowerCase()); }}
5. Fix the Loading Option Rendering
Instead of rendering an empty-value loading option, add a unique key to it and mark it as disabled to avoid accidental selection:
{this.state.loadingState ? <Option disabled key="loading">Loading.....</Option> : null}
Final Notes
- Pagination management: Track
currentPageandhasMoreDatastates to avoid infinite API calls when there's no more data to load. - Search reset: If your Select has search functionality, reset
currentPageanduserListwhen the input changes—otherwise, searching will append filtered results to the existing list instead of starting fresh. - Virtualization: If you're dealing with thousands of options, use Ant Design's
Select.Virtualcomponent (available in v4.20+) which renders only visible options, drastically improving performance.
内容的提问来源于stack exchange,提问作者Maneesha




