Skip to content

feat: use yielding to main thread in more places#2585

Closed
pauldambra wants to merge 2 commits intomainfrom
feat/more-defer
Closed

feat: use yielding to main thread in more places#2585
pauldambra wants to merge 2 commits intomainfrom
feat/more-defer

Conversation

@pauldambra
Copy link
Copy Markdown
Member

@pauldambra pauldambra commented Nov 15, 2025

we tested yielding processing to the main thread periodically and it worked well

reducing variability of INP on pages using that mode

let's abstract the task queue processing approach and use it in a few more places where we might be running over large arrays and cause a long task

@vercel
Copy link
Copy Markdown

vercel Bot commented Nov 15, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
posthog-js Error Error Dec 18, 2025 0:18am
posthog-nextjs-config Ready Ready Preview Dec 18, 2025 0:18am

Copy link
Copy Markdown
Member Author

pauldambra commented Nov 15, 2025

@pauldambra pauldambra marked this pull request as ready for review November 15, 2025 12:49
@pauldambra pauldambra requested a review from a team November 15, 2025 12:49
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment thread packages/browser/src/extensions/replay/external/network-plugin.ts Outdated
Comment thread packages/browser/src/utils/task-queue.ts Outdated
Comment thread packages/browser/src/utils/task-queue.ts Outdated
Comment thread packages/browser/src/utils/task-queue.ts Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Nov 15, 2025

Size Change: +26 kB (+0.49%)

Total Size: 5.33 MB

Filename Size Change
packages/browser/dist/array.full.es5.js 308 kB +2.54 kB (+0.83%)
packages/browser/dist/array.full.js 377 kB +1.91 kB (+0.51%)
packages/browser/dist/array.full.no-external.js 393 kB +2.29 kB (+0.59%)
packages/browser/dist/array.js 171 kB +2.21 kB (+1.31%)
packages/browser/dist/array.no-external.js 185 kB +2.58 kB (+1.41%)
packages/browser/dist/customizations.full.js 19.2 kB +58 B (+0.3%)
packages/browser/dist/dead-clicks-autocapture.js 13 kB -4 B (-0.03%)
packages/browser/dist/exception-autocapture.js 11.8 kB -4 B (-0.03%)
packages/browser/dist/lazy-recorder.js 152 kB +1.6 kB (+1.07%)
packages/browser/dist/main.js 172 kB +2.21 kB (+1.3%)
packages/browser/dist/module.full.js 377 kB +1.91 kB (+0.51%)
packages/browser/dist/module.full.no-external.js 393 kB +2.29 kB (+0.59%)
packages/browser/dist/module.js 172 kB +2.21 kB (+1.3%)
packages/browser/dist/module.no-external.js 186 kB +2.57 kB (+1.4%)
packages/browser/dist/posthog-recorder.js 249 kB +1.63 kB (+0.66%)
packages/browser/dist/product-tours.js 61 kB -4 B (-0.01%)
ℹ️ View Unchanged
Filename Size Change
packages/ai/dist/anthropic/index.cjs 17.8 kB 0 B
packages/ai/dist/anthropic/index.mjs 17.6 kB 0 B
packages/ai/dist/gemini/index.cjs 23.4 kB 0 B
packages/ai/dist/gemini/index.mjs 23.2 kB 0 B
packages/ai/dist/index.cjs 140 kB 0 B
packages/ai/dist/index.mjs 140 kB 0 B
packages/ai/dist/langchain/index.cjs 41.2 kB 0 B
packages/ai/dist/langchain/index.mjs 40.7 kB 0 B
packages/ai/dist/openai/index.cjs 42.3 kB 0 B
packages/ai/dist/openai/index.mjs 42 kB 0 B
packages/ai/dist/vercel/index.cjs 30 kB 0 B
packages/ai/dist/vercel/index.mjs 30 kB 0 B
packages/browser/dist/all-external-dependencies.js 229 kB 0 B
packages/browser/dist/conversations.js 37.6 kB 0 B
packages/browser/dist/crisp-chat-integration.js 2.11 kB 0 B
packages/browser/dist/external-scripts-loader.js 2.95 kB 0 B
packages/browser/dist/intercom-integration.js 2.16 kB 0 B
packages/browser/dist/recorder-v2.js 113 kB 0 B
packages/browser/dist/recorder.js 113 kB 0 B
packages/browser/dist/surveys-preview.js 72.9 kB 0 B
packages/browser/dist/surveys.js 85.1 kB 0 B
packages/browser/dist/tracing-headers.js 1.93 kB 0 B
packages/browser/dist/web-vitals.js 10.5 kB 0 B
packages/browser/react/dist/esm/index.js 19.3 kB 0 B
packages/browser/react/dist/umd/index.js 22.4 kB 0 B
packages/core/dist/error-tracking/chunk-ids.js 2.54 kB 0 B
packages/core/dist/error-tracking/chunk-ids.mjs 1.31 kB 0 B
packages/core/dist/error-tracking/coercers/dom-exception-coercer.js 2.3 kB 0 B
packages/core/dist/error-tracking/coercers/dom-exception-coercer.mjs 993 B 0 B
packages/core/dist/error-tracking/coercers/error-coercer.js 2.02 kB 0 B
packages/core/dist/error-tracking/coercers/error-coercer.mjs 794 B 0 B
packages/core/dist/error-tracking/coercers/error-event-coercer.js 1.76 kB 0 B
packages/core/dist/error-tracking/coercers/error-event-coercer.mjs 513 B 0 B
packages/core/dist/error-tracking/coercers/event-coercer.js 1.82 kB 0 B
packages/core/dist/error-tracking/coercers/event-coercer.mjs 548 B 0 B
packages/core/dist/error-tracking/coercers/index.js 6.79 kB 0 B
packages/core/dist/error-tracking/coercers/index.mjs 326 B 0 B
packages/core/dist/error-tracking/coercers/object-coercer.js 3.46 kB 0 B
packages/core/dist/error-tracking/coercers/object-coercer.mjs 2.07 kB 0 B
packages/core/dist/error-tracking/coercers/primitive-coercer.js 1.67 kB 0 B
packages/core/dist/error-tracking/coercers/primitive-coercer.mjs 419 B 0 B
packages/core/dist/error-tracking/coercers/promise-rejection-event.js 2.25 kB 0 B
packages/core/dist/error-tracking/coercers/promise-rejection-event.mjs 904 B 0 B
packages/core/dist/error-tracking/coercers/string-coercer.js 2.01 kB 0 B
packages/core/dist/error-tracking/coercers/string-coercer.mjs 820 B 0 B
packages/core/dist/error-tracking/coercers/utils.js 2.06 kB 0 B
packages/core/dist/error-tracking/coercers/utils.mjs 716 B 0 B
packages/core/dist/error-tracking/error-properties-builder.js 5.49 kB 0 B
packages/core/dist/error-tracking/error-properties-builder.mjs 4.15 kB 0 B
packages/core/dist/error-tracking/index.js 4.11 kB 0 B
packages/core/dist/error-tracking/index.mjs 152 B 0 B
packages/core/dist/error-tracking/parsers/base.js 1.83 kB 0 B
packages/core/dist/error-tracking/parsers/base.mjs 464 B 0 B
packages/core/dist/error-tracking/parsers/chrome.js 2.73 kB 0 B
packages/core/dist/error-tracking/parsers/chrome.mjs 1.32 kB 0 B
packages/core/dist/error-tracking/parsers/gecko.js 2.47 kB 0 B
packages/core/dist/error-tracking/parsers/gecko.mjs 1.13 kB 0 B
packages/core/dist/error-tracking/parsers/index.js 4.38 kB 0 B
packages/core/dist/error-tracking/parsers/index.mjs 1.94 kB 0 B
packages/core/dist/error-tracking/parsers/node.js 3.94 kB 0 B
packages/core/dist/error-tracking/parsers/node.mjs 2.68 kB 0 B
packages/core/dist/error-tracking/parsers/opera.js 2.26 kB 0 B
packages/core/dist/error-tracking/parsers/opera.mjs 746 B 0 B
packages/core/dist/error-tracking/parsers/safari.js 1.88 kB 0 B
packages/core/dist/error-tracking/parsers/safari.mjs 574 B 0 B
packages/core/dist/error-tracking/parsers/winjs.js 1.72 kB 0 B
packages/core/dist/error-tracking/parsers/winjs.mjs 426 B 0 B
packages/core/dist/error-tracking/types.js 1.33 kB 0 B
packages/core/dist/error-tracking/types.mjs 131 B 0 B
packages/core/dist/error-tracking/utils.js 1.8 kB 0 B
packages/core/dist/error-tracking/utils.mjs 604 B 0 B
packages/core/dist/eventemitter.js 1.78 kB 0 B
packages/core/dist/eventemitter.mjs 571 B 0 B
packages/core/dist/featureFlagUtils.js 6.5 kB 0 B
packages/core/dist/featureFlagUtils.mjs 4.28 kB 0 B
packages/core/dist/gzip.js 1.88 kB 0 B
packages/core/dist/gzip.mjs 577 B 0 B
packages/core/dist/index.js 5.7 kB 0 B
packages/core/dist/index.mjs 485 B 0 B
packages/core/dist/posthog-core-stateless.js 29.7 kB 0 B
packages/core/dist/posthog-core-stateless.mjs 27.2 kB 0 B
packages/core/dist/posthog-core.js 28.2 kB 0 B
packages/core/dist/posthog-core.mjs 24 kB 0 B
packages/core/dist/process/index.js 2.77 kB 0 B
packages/core/dist/process/index.mjs 114 B 0 B
packages/core/dist/process/spawn-local.js 1.82 kB 0 B
packages/core/dist/process/spawn-local.mjs 568 B 0 B
packages/core/dist/process/utils.js 3.12 kB 0 B
packages/core/dist/process/utils.mjs 1.15 kB 0 B
packages/core/dist/testing/index.js 2.93 kB 0 B
packages/core/dist/testing/index.mjs 79 B 0 B
packages/core/dist/testing/PostHogCoreTestClient.js 3.15 kB 0 B
packages/core/dist/testing/PostHogCoreTestClient.mjs 1.74 kB 0 B
packages/core/dist/testing/test-utils.js 2.77 kB 0 B
packages/core/dist/testing/test-utils.mjs 1.09 kB 0 B
packages/core/dist/types.js 8.2 kB 0 B
packages/core/dist/types.mjs 5.93 kB 0 B
packages/core/dist/utils/bot-detection.js 3.28 kB 0 B
packages/core/dist/utils/bot-detection.mjs 1.95 kB 0 B
packages/core/dist/utils/bucketed-rate-limiter.js 3 kB 0 B
packages/core/dist/utils/bucketed-rate-limiter.mjs 1.62 kB 0 B
packages/core/dist/utils/index.js 11.9 kB 0 B
packages/core/dist/utils/index.mjs 1.98 kB 0 B
packages/core/dist/utils/logger.js 2.5 kB 0 B
packages/core/dist/utils/logger.mjs 1.22 kB 0 B
packages/core/dist/utils/number-utils.js 2 kB 0 B
packages/core/dist/utils/number-utils.mjs 735 B 0 B
packages/core/dist/utils/promise-queue.js 2 kB 0 B
packages/core/dist/utils/promise-queue.mjs 768 B 0 B
packages/core/dist/utils/string-utils.js 1.91 kB 0 B
packages/core/dist/utils/string-utils.mjs 414 B 0 B
packages/core/dist/utils/type-utils.js 6.93 kB 0 B
packages/core/dist/utils/type-utils.mjs 3.03 kB 0 B
packages/core/dist/utils/user-agent-utils.js 14.9 kB 0 B
packages/core/dist/utils/user-agent-utils.mjs 11.9 kB 0 B
packages/core/dist/vendor/uuidv7.js 8.29 kB 0 B
packages/core/dist/vendor/uuidv7.mjs 6.72 kB 0 B
packages/nextjs-config/dist/config.js 4.97 kB 0 B
packages/nextjs-config/dist/config.mjs 3.48 kB 0 B
packages/nextjs-config/dist/index.js 2.24 kB 0 B
packages/nextjs-config/dist/index.mjs 30 B 0 B
packages/nextjs-config/dist/utils.js 3.83 kB 0 B
packages/nextjs-config/dist/utils.mjs 1.72 kB 0 B
packages/node/dist/client.js 25.3 kB 0 B
packages/node/dist/client.mjs 23.3 kB 0 B
packages/node/dist/entrypoints/index.edge.js 4.25 kB 0 B
packages/node/dist/entrypoints/index.edge.mjs 723 B 0 B
packages/node/dist/entrypoints/index.node.js 5.55 kB 0 B
packages/node/dist/entrypoints/index.node.mjs 1.08 kB 0 B
packages/node/dist/experimental.js 603 B 0 B
packages/node/dist/experimental.mjs 0 B 0 B 🆕
packages/node/dist/exports.js 3.6 kB 0 B
packages/node/dist/exports.mjs 124 B 0 B
packages/node/dist/extensions/context/context.js 2.12 kB 0 B
packages/node/dist/extensions/context/context.mjs 862 B 0 B
packages/node/dist/extensions/context/types.js 603 B 0 B
packages/node/dist/extensions/context/types.mjs 0 B 0 B 🆕
packages/node/dist/extensions/error-tracking/autocapture.js 2.66 kB 0 B
packages/node/dist/extensions/error-tracking/autocapture.mjs 1.24 kB 0 B
packages/node/dist/extensions/error-tracking/index.js 3.88 kB 0 B
packages/node/dist/extensions/error-tracking/index.mjs 2.61 kB 0 B
packages/node/dist/extensions/error-tracking/modifiers/context-lines.node.js 8.81 kB 0 B
packages/node/dist/extensions/error-tracking/modifiers/context-lines.node.mjs 7.15 kB 0 B
packages/node/dist/extensions/error-tracking/modifiers/module.node.js 2.78 kB 0 B
packages/node/dist/extensions/error-tracking/modifiers/module.node.mjs 1.45 kB 0 B
packages/node/dist/extensions/express.js 2.75 kB 0 B
packages/node/dist/extensions/express.mjs 1.16 kB 0 B
packages/node/dist/extensions/feature-flags/cache.js 603 B 0 B
packages/node/dist/extensions/feature-flags/cache.mjs 0 B 0 B 🆕
packages/node/dist/extensions/feature-flags/crypto.js 1.57 kB 0 B
packages/node/dist/extensions/feature-flags/crypto.mjs 395 B 0 B
packages/node/dist/extensions/feature-flags/feature-flags.js 30.9 kB 0 B
packages/node/dist/extensions/feature-flags/feature-flags.mjs 28.9 kB 0 B
packages/node/dist/extensions/sentry-integration.js 4.66 kB 0 B
packages/node/dist/extensions/sentry-integration.mjs 3.17 kB 0 B
packages/node/dist/storage-memory.js 1.52 kB 0 B
packages/node/dist/storage-memory.mjs 297 B 0 B
packages/node/dist/types.js 1.43 kB 0 B
packages/node/dist/types.mjs 224 B 0 B
packages/node/dist/version.js 1.21 kB 0 B
packages/node/dist/version.mjs 46 B 0 B
packages/nuxt/dist/module.mjs 4.27 kB 0 B
packages/nuxt/dist/runtime/composables/useFeatureFlagEnabled.js 566 B 0 B
packages/nuxt/dist/runtime/composables/useFeatureFlagPayload.js 597 B 0 B
packages/nuxt/dist/runtime/composables/useFeatureFlagVariantKey.js 591 B 0 B
packages/nuxt/dist/runtime/composables/usePostHog.js 128 B 0 B
packages/nuxt/dist/runtime/nitro-plugin.js 1.08 kB 0 B
packages/nuxt/dist/runtime/vue-plugin.js 1.14 kB 0 B
packages/react-native/dist/autocapture.js 5.05 kB 0 B
packages/react-native/dist/error-tracking/index.js 6.77 kB 0 B
packages/react-native/dist/error-tracking/utils.js 2.58 kB 0 B
packages/react-native/dist/frameworks/wix-navigation.js 1.3 kB 0 B
packages/react-native/dist/hooks/useFeatureFlag.js 1.49 kB 0 B
packages/react-native/dist/hooks/useFeatureFlags.js 821 B 0 B
packages/react-native/dist/hooks/useNavigationTracker.js 2.46 kB 0 B
packages/react-native/dist/hooks/usePostHog.js 467 B 0 B
packages/react-native/dist/index.js 3.12 kB 0 B
packages/react-native/dist/native-deps.js 8.05 kB 0 B
packages/react-native/dist/optional/OptionalAsyncStorage.js 299 B 0 B
packages/react-native/dist/optional/OptionalExpoApplication.js 377 B 0 B
packages/react-native/dist/optional/OptionalExpoDevice.js 347 B 0 B
packages/react-native/dist/optional/OptionalExpoFileSystem.js 386 B 0 B
packages/react-native/dist/optional/OptionalExpoFileSystemLegacy.js 423 B 0 B
packages/react-native/dist/optional/OptionalExpoLocalization.js 383 B 0 B
packages/react-native/dist/optional/OptionalReactNativeDeviceInfo.js 415 B 0 B
packages/react-native/dist/optional/OptionalReactNativeLocalize.js 303 B 0 B
packages/react-native/dist/optional/OptionalReactNativeNavigation.js 415 B 0 B
packages/react-native/dist/optional/OptionalReactNativeNavigationWix.js 443 B 0 B
packages/react-native/dist/optional/OptionalReactNativeSafeArea.js 644 B 0 B
packages/react-native/dist/optional/OptionalSessionReplay.js 455 B 0 B
packages/react-native/dist/posthog-rn.js 30.4 kB 0 B
packages/react-native/dist/PostHogContext.js 329 B 0 B
packages/react-native/dist/PostHogProvider.js 4.77 kB 0 B
packages/react-native/dist/storage.js 3.39 kB 0 B
packages/react-native/dist/surveys/components/BottomSection.js 1.34 kB 0 B
packages/react-native/dist/surveys/components/Cancel.js 909 B 0 B
packages/react-native/dist/surveys/components/ConfirmationMessage.js 1.65 kB 0 B
packages/react-native/dist/surveys/components/QuestionHeader.js 1.31 kB 0 B
packages/react-native/dist/surveys/components/QuestionTypes.js 10.9 kB 0 B
packages/react-native/dist/surveys/components/SurveyModal.js 3.86 kB 0 B
packages/react-native/dist/surveys/components/Surveys.js 7.18 kB 0 B
packages/react-native/dist/surveys/getActiveMatchingSurveys.js 2.64 kB 0 B
packages/react-native/dist/surveys/icons.js 7.76 kB 0 B
packages/react-native/dist/surveys/index.js 600 B 0 B
packages/react-native/dist/surveys/PostHogSurveyProvider.js 5.66 kB 0 B
packages/react-native/dist/surveys/surveys-utils.js 12.7 kB 0 B
packages/react-native/dist/surveys/useActivatedSurveys.js 3.67 kB 0 B
packages/react-native/dist/surveys/useSurveyStorage.js 2.16 kB 0 B
packages/react-native/dist/tooling/expoconfig.js 2.63 kB 0 B
packages/react-native/dist/tooling/metroconfig.js 2.2 kB 0 B
packages/react-native/dist/tooling/posthogMetroSerializer.js 4.78 kB 0 B
packages/react-native/dist/tooling/utils.js 4.05 kB 0 B
packages/react-native/dist/tooling/vendor/expo/expoconfig.js 70 B 0 B
packages/react-native/dist/tooling/vendor/metro/countLines.js 237 B 0 B
packages/react-native/dist/tooling/vendor/metro/utils.js 3.35 kB 0 B
packages/react-native/dist/types.js 70 B 0 B
packages/react-native/dist/utils.js 539 B 0 B
packages/react-native/dist/version.js 130 B 0 B
packages/react/dist/esm/index.js 19.3 kB 0 B
packages/react/dist/umd/index.js 22.4 kB 0 B
packages/rollup-plugin/dist/index.js 3.45 kB 0 B
packages/web/dist/index.cjs 13.8 kB 0 B
packages/web/dist/index.mjs 13.7 kB 0 B
packages/webpack-plugin/dist/config.js 2.65 kB 0 B
packages/webpack-plugin/dist/config.mjs 1.64 kB 0 B
packages/webpack-plugin/dist/index.js 6.38 kB 0 B
packages/webpack-plugin/dist/index.mjs 2.96 kB 0 B
tooling/changelog/dist/index.js 3.31 kB 0 B
tooling/rollup-utils/dist/index.js 1.17 kB 0 B

compressed-size-action

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a new TaskQueue utility to improve page responsiveness by breaking up CPU-intensive operations and yielding to the main thread periodically. This helps reduce Interaction to Next Paint (INP) variability on pages with heavy processing.

  • Adds a reusable TaskQueue class with time-budgeting (default 30ms) to avoid blocking the main thread
  • Implements helper functions processWithYield and processAsyncWithYield for common array processing patterns
  • Integrates the task queue into request queue flushing, extension initialization, performance entry processing, and session recording event handling

Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/browser/src/utils/task-queue.ts New task queue implementation with time-slicing support and helper functions for processing arrays with yielding
packages/browser/src/utils/tests/task-queue.test.ts Comprehensive test suite covering task queue functionality, error handling, and yielding behavior
packages/browser/src/request-queue.ts Updates flush callback to use processWithYield for processing request batches
packages/browser/src/posthog-core.ts Refactors extension initialization to use TaskQueue class, removing custom time-slicing logic
packages/browser/src/extensions/replay/external/network-plugin.ts Applies yielding to initial performance entry processing
packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts Adds yielding to queued event processing
.changeset/smooth-wolves-mix.md Changeset documenting the new feature

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/browser/src/utils/task-queue.ts Outdated
Comment thread packages/browser/src/utils/task-queue.ts Outdated
Comment thread packages/browser/src/utils/task-queue.ts Outdated
Comment thread packages/browser/src/extensions/replay/external/network-plugin.ts Outdated
Comment thread packages/browser/src/request-queue.ts Outdated
Comment thread packages/browser/src/utils/task-queue.ts Outdated
Comment thread packages/browser/src/utils/task-queue.ts Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/browser/src/site-apps.ts Outdated
Comment thread packages/browser/src/retry-queue.ts Outdated
Comment thread packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts Outdated
Comment thread packages/browser/src/utils/task-queue.ts Outdated
Comment thread packages/browser/jest.config.js
Comment thread packages/browser/src/posthog-core.ts Outdated
Comment thread packages/browser/src/utils/task-queue.ts Outdated
Comment thread playground/remix/app/providers.tsx Outdated
Copy link
Copy Markdown
Member Author

pauldambra commented Dec 11, 2025

Local INP testing Results Summary

Test npm (no scheduler) local (scheduler) Max INP Change
Simple page 56ms max 48ms max -14%
Media page 64ms max 48ms max -25%

Copy link
Copy Markdown
Member

@rafaeelaudibert rafaeelaudibert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, left some nits

supportedCompression: ['gzip'],
} as RemoteConfig
supportedCompression: [Compression.GZipJS],
} as Partial<RemoteConfig> as RemoteConfig
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird casting? Really needed?

for (const key in requests) {
const req = requests[key]
const now = new Date().getTime()
const requestEntries = Object.entries(requests)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You only use the values, so Object.values is all you need here

logger.info(`Processing ${this._bufferedInvocations.length} events for site app with id ${loader.id}`)
this._bufferedInvocations.forEach((globals) => app.processEvent?.(globals))
app.processedBuffer = true
scheduler.processEach(this._bufferedInvocations, (globals) => app.processEvent?.(globals), {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You reacted with a +1 to my comment but didnt really explain whether keeping this duplicated was intended or not


type IdleCallbackHandle = number | ReturnType<typeof setTimeout>

export const _requestIdleCallback = (
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment explaining the fallback and difference between requestIdleCallback and setTimeout wouldnt hurt

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jan 5, 2026

This PR hasn't seen activity in a week! Should it be merged, closed, or further worked on? If you want to keep it open, post a comment or remove the stale label – otherwise this will be closed in another week.

@github-actions
Copy link
Copy Markdown
Contributor

This PR was closed due to lack of activity. Feel free to reopen if it's still relevant.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jan 13, 2026

Greptile Summary

This PR introduces a new Scheduler utility to yield processing to the main thread periodically, improving INP (Interaction to Next Paint) performance. The scheduler is used in request flushing, retry processing, site apps, and session recording initialization.

Key Changes

  • New Scheduler utility (scheduler.ts): Implements time-slicing with high/normal priority queues

    • High priority: Uses setTimeout(0) with 30ms time budget
    • Normal priority: Uses requestIdleCallback
    • Activates only when __preview_extra_scheduling config is enabled
  • Deferred extension initialization: Refactored to use the scheduler for time-slicing initialization tasks

  • Request/Retry queue processing: Now uses scheduler for batch processing

Critical Issues Found

⚠️ Data Loss Bug: Using the scheduler for request/retry queue flushing creates a race condition with page unload. When requests are being processed asynchronously by the scheduler and the user navigates away, those events are lost because:

  1. The queue is cleared before passing items to the scheduler
  2. The scheduler processes asynchronously (yielding every 30ms)
  3. unload() checks an already-empty queue and doesn't wait for the scheduler
  4. Events still in the scheduler are abandoned

This affects both RequestQueue (lines 68-80) and RetryQueue (lines 146-148).

Other Concerns

  • Starvation risk: Normal priority tasks may never execute if high priority tasks are continuously added
  • Feature flag confusion: The __preview_extra_scheduling flag means this feature won't be used in production by default, despite PR description suggesting it's ready
  • Test changes: Switching to jest.runAllTimers() could cause tests to hang if scheduler has infinite scheduling bugs

Recommendation

The PR implements a good architectural pattern for main thread yielding, but the critical data loss bug in request/retry queues must be fixed before merge. These queues should either:

  1. Process synchronously (original behavior)
  2. Track pending scheduler work so unload() can flush it
  3. Not use the scheduler for "must complete before unload" scenarios

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +68 to +80
scheduler.processEach(
requestEntries,
([, req]) => {
if (req.data && isArray(req.data)) {
each(req.data, (data) => {
data['offset'] = Math.abs(data['timestamp'] - flushStartTime)
delete data['timestamp']
})
}
this._sendRequest(req)
},
{ priority: 'high' }
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical data loss bug: Using scheduler.processEach here creates a race condition with the unload() method that can result in event loss.

The problem:

  1. When this timeout fires, _formatQueue() is called (line 64), which clears this._queue (line 103 in _formatQueue)
  2. The formatted requests are then passed to scheduler.processEach to be sent asynchronously
  3. The scheduler yields every 30ms to avoid blocking the main thread (scheduler.ts line 87)
  4. If the user navigates away/closes the page during this yielding period:
    • _handle_unload() is called (posthog-core.ts line 870)
    • Which calls this._requestQueue?.unload() (posthog-core.ts line 884)
    • unload() checks this._queue.length > 0 (line 39)
    • But this._queue is already empty (cleared in step 1)
    • So it doesn't send the events that are still queued in the scheduler
    • Those events are lost forever

The fix:
The scheduler should not be used for request flushing that needs to complete before page unload. Either:

  1. Process requests synchronously in the flush (original behavior)
  2. Or track pending scheduler work separately so unload() can flush it
  3. Or don't use the scheduler when priority: 'high' means "must complete before unload"

This is particularly critical because the comment in scheduler.ts line 65 explicitly mentions "flushing events on page unload" as a use case for high priority.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browser/src/request-queue.ts
Line: 68:80

Comment:
Critical data loss bug: Using `scheduler.processEach` here creates a race condition with the `unload()` method that can result in event loss.

**The problem:**
1. When this timeout fires, `_formatQueue()` is called (line 64), which clears `this._queue` (line 103 in _formatQueue)
2. The formatted requests are then passed to `scheduler.processEach` to be sent asynchronously
3. The scheduler yields every 30ms to avoid blocking the main thread (scheduler.ts line 87)
4. If the user navigates away/closes the page during this yielding period:
   - `_handle_unload()` is called (posthog-core.ts line 870)
   - Which calls `this._requestQueue?.unload()` (posthog-core.ts line 884)
   - `unload()` checks `this._queue.length > 0` (line 39)
   - But `this._queue` is already empty (cleared in step 1)
   - So it doesn't send the events that are still queued in the scheduler
   - **Those events are lost forever**

**The fix:**
The scheduler should not be used for request flushing that needs to complete before page unload. Either:
1. Process requests synchronously in the flush (original behavior)
2. Or track pending scheduler work separately so `unload()` can flush it
3. Or don't use the scheduler when `priority: 'high'` means "must complete before unload"

This is particularly critical because the comment in scheduler.ts line 65 explicitly mentions "flushing events on page unload" as a use case for high priority.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +146 to +148
scheduler.processEach(toFlush, ({ requestOptions }) => this.retriableRequest(requestOptions), {
priority: 'high',
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same critical data loss bug as in request-queue.ts: Using scheduler.processEach here with asynchronous processing means that if the page unloads while the scheduler is processing retries, those retry attempts will be abandoned.

When unload() is called (line 152), it iterates through this._queue and sends requests via sendBeacon. However, if a flush was in progress via the scheduler:

  • The retry items were removed from this._queue (line 143: this._queue = notToFlush)
  • They're sitting in the scheduler's queue waiting to be processed
  • The scheduler won't complete before the page unloads
  • Those retry attempts are lost

The original synchronous for loop ensured all retries were at least attempted before moving on. The scheduler breaks this guarantee.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browser/src/retry-queue.ts
Line: 146:148

Comment:
Same critical data loss bug as in request-queue.ts: Using `scheduler.processEach` here with asynchronous processing means that if the page unloads while the scheduler is processing retries, those retry attempts will be abandoned.

When `unload()` is called (line 152), it iterates through `this._queue` and sends requests via sendBeacon. However, if a flush was in progress via the scheduler:
- The retry items were removed from `this._queue` (line 143: `this._queue = notToFlush`)
- They're sitting in the scheduler's queue waiting to be processed
- The scheduler won't complete before the page unloads
- **Those retry attempts are lost**

The original synchronous `for` loop ensured all retries were at least attempted before moving on. The scheduler breaks this guarantee.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +101 to +108
private _processNormal(deadline: { timeRemaining: () => number }): void {
this._normalScheduled = null

while (this._normalQueue.length > 0) {
// Always let high priority tasks run first
if (this._highQueue.length > 0) {
this._normalScheduled = _requestIdleCallback((d) => this._processNormal(d))
return
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential starvation issue: When processing normal priority tasks, if high priority tasks keep getting added, normal priority tasks will keep yielding and may never complete.

The current logic:

  1. Check if high priority queue has tasks
  2. If yes, reschedule normal priority processing and return
  3. This repeats every time normal processing is attempted

If high priority tasks are added continuously (e.g., during heavy user interaction), normal priority tasks could be starved indefinitely. Consider adding a mechanism to:

  • Process at least some normal priority tasks before yielding
  • Or track how many times a normal priority task has been deferred and eventually force process it
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browser/src/utils/scheduler.ts
Line: 101:108

Comment:
Potential starvation issue: When processing normal priority tasks, if high priority tasks keep getting added, normal priority tasks will keep yielding and may never complete.

The current logic:
1. Check if high priority queue has tasks
2. If yes, reschedule normal priority processing and return
3. This repeats every time normal processing is attempted

If high priority tasks are added continuously (e.g., during heavy user interaction), normal priority tasks could be starved indefinitely. Consider adding a mechanism to:
- Process at least some normal priority tasks before yielding
- Or track how many times a normal priority task has been deferred and eventually force process it

How can I resolve this? If you propose a fix, please make it concise.

})
)

scheduler.setActive(!!this.config.__preview_extra_scheduling)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scheduler is only activated when __preview_extra_scheduling is enabled, which means the new time-slicing behavior won't be used in production by default. However, the PR description states "we tested yielding processing to the main thread periodically and it worked well" and suggests it's ready for broader use.

Consider:

  1. Is this intentionally behind a feature flag for gradual rollout?
  2. If so, the flag name with __preview_ suggests it's experimental - consider a clearer flag name
  3. If the feature is ready, consider enabling it by default or documenting the rollout plan

Additionally, note that when the scheduler is inactive (_active: false), all the scheduler-using code (request-queue, retry-queue, site-apps, lazy-loaded-session-recorder) will process synchronously, which means they don't get the INP benefits mentioned in the PR description.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browser/src/posthog-core.ts
Line: 521:521

Comment:
The scheduler is only activated when `__preview_extra_scheduling` is enabled, which means the new time-slicing behavior won't be used in production by default. However, the PR description states "we tested yielding processing to the main thread periodically and it worked well" and suggests it's ready for broader use.

Consider:
1. Is this intentionally behind a feature flag for gradual rollout?
2. If so, the flag name with `__preview_` suggests it's experimental - consider a clearer flag name
3. If the feature is ready, consider enabling it by default or documenting the rollout plan

Additionally, note that when the scheduler is inactive (`_active: false`), all the scheduler-using code (request-queue, retry-queue, site-apps, lazy-loaded-session-recorder) will process synchronously, which means they don't get the INP benefits mentioned in the PR description.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +23 to +28
const fastForwardTimeAndRunTimer = async (time = 3500) => {
now += time
jest.setSystemTime(now)
jest.runOnlyPendingTimers()
jest.runAllTimers()
// eslint-disable-next-line compat/compat
await Promise.resolve()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix to use jest.runAllTimers() instead of jest.runOnlyPendingTimers() since the scheduler creates new timers during processing. However, this introduces a risk: if there's a bug in the scheduler that causes it to continuously schedule work, tests will hang indefinitely.

Consider adding a timeout to these tests (e.g., jest.setTimeout(5000)) to catch infinite scheduling bugs early.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browser/src/__tests__/retry-queue.test.ts
Line: 23:28

Comment:
Good fix to use `jest.runAllTimers()` instead of `jest.runOnlyPendingTimers()` since the scheduler creates new timers during processing. However, this introduces a risk: if there's a bug in the scheduler that causes it to continuously schedule work, tests will hang indefinitely.

Consider adding a timeout to these tests (e.g., `jest.setTimeout(5000)`) to catch infinite scheduling bugs early.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@github-actions
Copy link
Copy Markdown
Contributor

This PR hasn't seen activity in a week! Should it be merged, closed, or further worked on? If you want to keep it open, post a comment or remove the stale label – otherwise this will be closed in another week.

@github-actions
Copy link
Copy Markdown
Contributor

This PR was closed due to lack of activity. Feel free to reopen if it's still relevant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants