Skip to content

Fix race conditions, memory leaks, and performance issues in lazy-define#337

Draft
Copilot wants to merge 9 commits intomainfrom
copilot/fix-performance-issues-lazy-define
Draft

Fix race conditions, memory leaks, and performance issues in lazy-define#337
Copilot wants to merge 9 commits intomainfrom
copilot/fix-performance-issues-lazy-define

Conversation

Copy link
Contributor

Copilot AI commented Feb 17, 2026

The lazy-define system had multiple concurrency and resource management issues causing duplicate callback execution, memory leaks from undisconnected observers, and inefficient DOM scanning.

Core Fixes

Race condition prevention

  • Introduced triggered Set to atomically claim tags before async work
  • Move pending.delete() before strategy execution to prevent concurrent scans from double-firing callbacks

IntersectionObserver leak

  • visible() strategy created observers that never resolved when elements didn't exist
  • Added MutationObserver fallback that waits for elements to appear in DOM before creating IntersectionObserver

Resource cleanup

  • MutationObserver remained active after all tags processed
  • Added cleanupObserver() to disconnect when pending.size === 0
  • Track observed targets in WeakSet to prevent redundant observe() calls

Late registration handling

  • Callbacks registered after tag triggering were never executed
  • Check triggered Set in lazyDefine() and immediately schedule via Promise.resolve().then(cb)

Error handling

  • Wrap callback execution in try-catch with .catch(reportError) to prevent unhandled rejections

Scan optimization

  • Early-exit when pending.size === 0
  • Snapshot pending.keys() to allow safe deletion during iteration

Example

// Before: Could fire callbacks multiple times
lazyDefine('my-element', onDefine)
document.body.innerHTML = '<my-element></my-element><my-element></my-element>'

// Before: Memory leak when element doesn't exist yet
lazyDefine('future-element', onDefine) // IntersectionObserver created but never resolved

// After: Both issues fixed with triggered Set and MutationObserver fallback

All changes maintain the existing public API - this is a drop-in replacement.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • accounts.google.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=3931 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/puppeteer_dev_chrome_profile-IHbIRg --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,12727165736899854248,416083540779988293,262144 --enable-features=PdfOopif --disable-features=AcceptCHFrame,IsolateSandboxedIframes,MediaRouter,OptimizationHints,PaintHolding,ProcessPerSiteUpToMainFrameThreshold,RenderDocument,Translate --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
    • Triggering command: /opt/google/chrome/chrome /usr/bin/google-chrome-stable --allow-pre-commit-input --disable-REDACTED-networking --disable-REDACTED-timer-throttling --disable-REDACTEDing-occluded-windows --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-REDACTED-pages --disable-crash-reporter --disable-default-apps --disable-dev-shm-usage --disable-hang-monitor --disable-infobars --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-REDACTEDing --disable-search-engine-choice-screen --disable-sync --enable-automation (dns block)
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=4462 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/puppeteer_dev_chrome_profile-IjcDW3 --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,7595176966221360470,7760476294586490019,262144 --enable-features=PdfOopif --disable-features=AcceptCHFrame,IsolateSandboxedIframes,MediaRouter,OptimizationHints,PaintHolding,ProcessPerSiteUpToMainFrameThreshold,RenderDocument,Translate --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
  • android.clients.google.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=4462 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/puppeteer_dev_chrome_profile-IjcDW3 --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,7595176966221360470,7760476294586490019,262144 --enable-features=PdfOopif --disable-features=AcceptCHFrame,IsolateSandboxedIframes,MediaRouter,OptimizationHints,PaintHolding,ProcessPerSiteUpToMainFrameThreshold,RenderDocument,Translate --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
    • Triggering command: /opt/google/chrome/chrome /usr/bin/google-chrome-stable --allow-pre-commit-input --disable-REDACTED-networking --disable-REDACTED-timer-throttling --disable-REDACTEDing-occluded-windows --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-REDACTED-pages --disable-crash-reporter --disable-default-apps --disable-dev-shm-usage --disable-hang-monitor --disable-infobars --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-REDACTEDing --disable-search-engine-choice-screen --disable-sync --enable-automation (dns block)
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=4917 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/puppeteer_dev_chrome_profile-JMVZOS --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,14336023522143949526,1759897775106439465,262144 --enable-features=PdfOopif --disable-features=AcceptCHFrame,IsolateSandboxedIframes,MediaRouter,OptimizationHints,PaintHolding,ProcessPerSiteUpToMainFrameThreshold,RenderDocument,Translate --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
  • clients2.google.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=3931 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/puppeteer_dev_chrome_profile-IHbIRg --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,12727165736899854248,416083540779988293,262144 --enable-features=PdfOopif --disable-features=AcceptCHFrame,IsolateSandboxedIframes,MediaRouter,OptimizationHints,PaintHolding,ProcessPerSiteUpToMainFrameThreshold,RenderDocument,Translate --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
    • Triggering command: /opt/google/chrome/chrome /usr/bin/google-chrome-stable --allow-pre-commit-input --disable-REDACTED-networking --disable-REDACTED-timer-throttling --disable-REDACTEDing-occluded-windows --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-REDACTED-pages --disable-crash-reporter --disable-default-apps --disable-dev-shm-usage --disable-hang-monitor --disable-infobars --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-REDACTEDing --disable-search-engine-choice-screen --disable-sync --enable-automation (dns block)
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=4462 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/puppeteer_dev_chrome_profile-IjcDW3 --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,7595176966221360470,7760476294586490019,262144 --enable-features=PdfOopif --disable-features=AcceptCHFrame,IsolateSandboxedIframes,MediaRouter,OptimizationHints,PaintHolding,ProcessPerSiteUpToMainFrameThreshold,RenderDocument,Translate --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
  • https://api.github.com/graphql
    • Triggering command: `/usr/bin/gh gh pr ready --undo copilot/fix-performance-issues-lazy-define --noprofile sh ndor/bin/git web-test-REDACTED /bin/java rgo/bin/git sh -c web-test-REDACTED --files=test/lazy-define.ts gets race condition and cleanup properly

Co-authored-by: mattcosta7 <8616962+mattcosta7@users.n/proc/1350/fd ich c5a185d8:src/lazgit HEAD 9905e882b8d1a06c. git` (http block)

  • Triggering command: `/usr/bin/gh gh pr view copilot/fix-performance-issues-lazy-define --json isDraft,number,title /lib/jspawnhelpeHEAD rgo/bin/git web-test-REDACTED bash git sh -c web-test-REDACTED --files=test/lazy-define.ts gets race condition and cleanup properly

Co-authored-by: mattcosta7 <8616962+mattcosta7@users.n--norc tnet/tools/which c5a185d8:src/lazgit HEAD de/node/bin/npm bash` (http block)

  • safebrowsingohttpgateway.googleapis.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=3931 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/puppeteer_dev_chrome_profile-IHbIRg --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,12727165736899854248,416083540779988293,262144 --enable-features=PdfOopif --disable-features=AcceptCHFrame,IsolateSandboxedIframes,MediaRouter,OptimizationHints,PaintHolding,ProcessPerSiteUpToMainFrameThreshold,RenderDocument,Translate --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
    • Triggering command: /opt/google/chrome/chrome /usr/bin/google-chrome-stable --allow-pre-commit-input --disable-REDACTED-networking --disable-REDACTED-timer-throttling --disable-REDACTEDing-occluded-windows --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-REDACTED-pages --disable-crash-reporter --disable-default-apps --disable-dev-shm-usage --disable-hang-monitor --disable-infobars --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-REDACTEDing --disable-search-engine-choice-screen --disable-sync --enable-automation (dns block)
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=4462 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/puppeteer_dev_chrome_profile-IjcDW3 --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,7595176966221360470,7760476294586490019,262144 --enable-features=PdfOopif --disable-features=AcceptCHFrame,IsolateSandboxedIframes,MediaRouter,OptimizationHints,PaintHolding,ProcessPerSiteUpToMainFrameThreshold,RenderDocument,Translate --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
  • www.google.com
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=3931 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/puppeteer_dev_chrome_profile-IHbIRg --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,12727165736899854248,416083540779988293,262144 --enable-features=PdfOopif --disable-features=AcceptCHFrame,IsolateSandboxedIframes,MediaRouter,OptimizationHints,PaintHolding,ProcessPerSiteUpToMainFrameThreshold,RenderDocument,Translate --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)
    • Triggering command: /opt/google/chrome/chrome /usr/bin/google-chrome-stable --allow-pre-commit-input --disable-REDACTED-networking --disable-REDACTED-timer-throttling --disable-REDACTEDing-occluded-windows --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-REDACTED-pages --disable-crash-reporter --disable-default-apps --disable-dev-shm-usage --disable-hang-monitor --disable-infobars --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-REDACTEDing --disable-search-engine-choice-screen --disable-sync --enable-automation (dns block)
    • Triggering command: /proc/self/exe /proc/self/exe --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --disable-dev-shm-usage --use-angle=swiftshader-webgl --mute-audio --crashpad-handler-pid=4462 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/puppeteer_dev_chrome_profile-IjcDW3 --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,7595176966221360470,7760476294586490019,262144 --enable-features=PdfOopif --disable-features=AcceptCHFrame,IsolateSandboxedIframes,MediaRouter,OptimizationHints,PaintHolding,ProcessPerSiteUpToMainFrameThreshold,RenderDocument,Translate --variations-seed-version --trace-process-track-uuid=3190708989122997041 (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

Problem

src/lazy-define.ts has several performance and correctness issues that need to be addressed:

1. Race condition: duplicate callback firing (High)

dynamicElements.delete(tagName) runs inside the requestAnimationFrame callback, so a second scan() on a different element can iterate the same tag before deletion, causing callbacks to fire multiple times.

Fix: Introduce a synchronous triggered set. triggerTag() atomically claims a tag name before any async work, so no concurrent scan can double-fire. Delete from pending before awaiting the strategy.

2. IntersectionObserver leak when no elements match (High)

In the visible() strategy, if document.querySelectorAll(tagName) returns no elements, the IntersectionObserver is created but never observes anything, so the promise never resolves and the observer/closure are retained forever.

Fix: Instead of resolving immediately (which would defeat the purpose of visible for not-yet-rendered elements), use a MutationObserver fallback to wait for the element to appear in the DOM before creating the IntersectionObserver:

function visible(tagName: string): Promise<void> {
  return new Promise<void>(resolve => {
    function tryObserve() {
      const elements = document.querySelectorAll(tagName)
      if (elements.length === 0) return false

      const observer = new IntersectionObserver(
        entries => {
          for (const entry of entries) {
            if (entry.isIntersecting) {
              resolve()
              observer.disconnect()
              return
            }
          }
        },
        {
          rootMargin: '0px 0px 256px 0px',
          threshold: 0.01
        }
      )
      for (const el of elements) observer.observe(el)
      return true
    }

    if (tryObserve()) return

    const mo = new MutationObserver(() => {
      if (tryObserve()) mo.disconnect()
    })
    mo.observe(document.documentElement, {subtree: true, childList: true})
  })
}

3. Redundant observe() calls (Medium)

No guard against calling observe() on the same target multiple times. The same MutationObserver would observe the same subtree redundantly, causing duplicate mutation records and duplicate scan() calls.

Fix: Use a WeakSet<ElementLike> to track already-observed targets. Re-calling observe() still triggers a scan() (to pick up newly registered tags) but skips the duplicate observer.observe().

4. MutationObserver is never disconnected (Medium)

Even after every registered tag has been triggered and pending is empty, the observer remains connected to the document, receiving and discarding every DOM mutation.

Fix: Disconnect the observer when pending is empty after the last tag is triggered. Reconnect when new tags are registered via lazyDefine.

5. No error handling on callbacks (Medium)

If any callback throws, the rejection is unhandled.

Fix: Add .catch(reportError) (or equivalent) on callback promises to surface errors without killing other callbacks.

6. Late registration after trigger (Medium)

If lazyDefine is called for a tag that has already been found and triggered, the callback is added to a set that will never be consumed.

Fix: In lazyDefine, check the triggered set and immediately schedule the callback via Promise.resolve().then(cb) for already-triggered tags, preserving the async-microtask contract.

7. Scan optimization (Low-Medium)

Every added node iterates every registered tag name.

Fix: Early-exit when pending.size === 0, snapshot keys with [...pending.keys()] for safe in-loop mutation so triggered tags are removed and subsequent scans iterate fewer tags.

Requirements

  • The public API (lazyDefine and observe exports) must remain unchanged — this should be a drop-in replacement.
  • Keep the PR as a draft when it's ready.
  • Update or add tests if applicable to cover the race condition and leak fixes.
  • The file to modify is src/lazy-define.ts.

This pull request was created from Copilot chat.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 8 commits February 17, 2026 03:51
Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
- Fix race condition by using triggered Set to prevent duplicate callbacks
- Add observedTargets WeakSet to avoid redundant MutationObserver calls
- Disconnect MutationObserver when no pending elements remain
- Add comprehensive error handling with try-catch and .catch(reportError)
- Implement late registration check for already-triggered elements
- Optimize scan() with early return and snapshot iteration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix redundant check in late registration logic
- Use for-of loops instead of traditional for loops for better readability

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The visible() function had a memory leak when querySelectorAll returned
no elements - the IntersectionObserver was created but never observed
anything, so the promise never resolved and the observer/closure were
retained forever.

Fix by:
- First checking if elements exist
- If they exist, create IntersectionObserver (original behavior)
- If they don't exist, create MutationObserver to watch for DOM insertions
- When element appears, disconnect MutationObserver and create IntersectionObserver

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix performance and correctness issues in lazy-define Fix race conditions, memory leaks, and performance issues in lazy-define Feb 17, 2026
Copilot AI requested a review from mattcosta7 February 17, 2026 04:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants