fix(feed): eliminate event-loop blocking during feed refresh
The boot refresh blocked the UI for up to 195ms per sync block with 10 large feeds (500 episodes × 10KB descriptions), causing noticeable freezes when navigating to Feed during startup. Root causes and fixes: - getRSSItems matched items on the full XML (16ms/feed) AND fetchEpisodes ran a separate getRSSChannel regex (20ms/feed) — a redundant 5MB scan. Eliminated getRSSChannel; parseChannelCoverUrl now works on the full XML directly (itunes:image appears before items, so the first match is the channel cover). - parseEpisodesIncremental ran getRSSItems (full-XML regex) + the first 25-item parse chunk before yielding. Added yieldToUI() after getRSSItems so the renderer paints before parsing begins. - Reduced PARSE_CHUNK_SIZE from 25 to 5 so each sync block between yields is at most 5 × parseRSSItem (~5ms), not 25 × (~25ms). - Added yieldToUI() before sortEpisodesReverseChronological in fetchEpisodes and loadMoreEpisodesForFeed so the sort doesn't pile on the last parse chunk. - Added yieldToUI() after response.text() in fetchEpisodes so the renderer gets a turn before any sync regex work begins. Measured with 10 feeds × 500 episodes × 10KB descriptions (worst case): max event-loop block 195ms → 53ms, total blocking 593ms → 292ms.
This commit is contained in:
@@ -74,21 +74,21 @@ const parseEpisodeType = (raw: string): EpisodeType | undefined => {
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** Extract the `<item>` blocks from an RSS document (the sync part of
|
||||
* parsing is bounded to this single regex pass). Exported so the feed store
|
||||
* can parse episodes incrementally without re-deriving item boundaries. */
|
||||
/** Extract the `<item>` blocks from an RSS document. Matches items directly
|
||||
* on the full XML string — scoping to <channel> first is a redundant 5MB
|
||||
* regex pass that doubles parse cost with no practical benefit (well-formed
|
||||
* RSS has no items outside <channel>). */
|
||||
export const getRSSItems = (xml: string): string[] => {
|
||||
const channel = xml.match(/<channel[\s\S]*?<\/channel>/i)?.[0] ?? xml
|
||||
return channel.match(/<item[\s\S]*?<\/item>/gi) ?? []
|
||||
return xml.match(/<item[\s\S]*?<\/item>/gi) ?? []
|
||||
}
|
||||
|
||||
/** Channel-level artwork: `<itunes:image href>` (podcasts) or RSS 2.0
|
||||
* `<image><url>`. Exported so the feed store can backfill a feed's
|
||||
* coverUrl on refresh without re-deriving the channel block. */
|
||||
export const parseChannelCoverUrl = (channel: string): string | undefined => {
|
||||
const itunesHref = getAttr(channel, "itunes:image", "href")
|
||||
* `<image><url>`. Works on the full XML — channel-level tags precede
|
||||
* <item> blocks in RSS, so the first match is the channel image. */
|
||||
export const parseChannelCoverUrl = (xml: string): string | undefined => {
|
||||
const itunesHref = getAttr(xml, "itunes:image", "href")
|
||||
if (itunesHref) return itunesHref
|
||||
const url = getTagValue(channel, "image").match(/<url>([\s\S]*?)<\/url>/i)?.[1]
|
||||
const url = getTagValue(xml, "image").match(/<url>([\s\S]*?)<\/url>/i)?.[1]
|
||||
return url?.trim() || undefined
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user