Your loop yields every 500 items. It still freezes. Here's why (and a fix)
Every JavaScript developer eventually writes this loop: for ( const [ index , row ] of rows . entries ()) { processRow ( row ); if ( index % 500 === 0 ) await new Promise ( resolve => setImmediate ( resolve )); } It's the textbook fix for a long loop that freezes the UI or stalls a server: do some work, give the event loop a turn, repeat. It works in testing. It works in staging. Then production…
Every JavaScript developer eventually writes a loop that processes a large dataset by yielding control to the event loop every 500 items. This technique aims to prevent the UI from freezing or stalling the server while the loop executes. The basic idea is to perform some work, give the event loop a chance to run, and repeat the process.
This method works in testing and staging environments. However, when the real-world data is introduced, the app may still freeze due to a cluster of heavy items appearing together in the loop.
The problem with the traditional approach lies in the assumption that each item costs the same amount of time to process. In reality, most items are small and take only microseconds to process, while a few items are heavy and take longer to handle. When a batch of heavy items shows up consecutively, the loop takes longer to complete, causing the app to freeze during that time.
To address this issue, the author developed the time-sliced library, which yields control based on time rather than the number of items. Instead of counting items, the library measures the time spent on processing each item. If the processing budget is spent (e.g., 8 ms by default), the library yields control to the event loop before continuing with the next item. This approach ensures that the app remains responsive even when encountering slow items in the dataset.
The API for the time-sliced library is simple. Developers can import the timeSlicedForEach function and apply it to their loop, specifying the rows to process and the function to execute for each row. The library provides a default budget of 8 ms per iteration, but developers can adjust this value based on their specific requirements.
In a real-world scenario, the author tested the time-sliced library with a dataset containing 20,000 rows, of which 1,000 were heavy and took 0.5 ms to process. The library was compared to several other common techniques, including a plain loop, async.eachSeries, setTimeout(0) every row, and setImmediate with 500-item intervals. The results showed that the time-sliced library completed the task in 921 ms with only an 11 ms event loop freeze, which was much faster than the other methods and prevented the app from freezing.
The library can be used with various types of loops, including nested, while, and recursive loops. For example, it can be used to update a search index or process CSV rows, ensuring that the app remains responsive during these operations. Additionally, the time-sliced library can be canceled using an AbortController, making it ideal for use cases such as search-as-you-type functionality.
Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.