perf(browser): reduce runtime memory footprint and GC pressure by ~54%#3255
Draft
marandaneto wants to merge 4 commits intomainfrom
Draft
perf(browser): reduce runtime memory footprint and GC pressure by ~54%#3255marandaneto wants to merge 4 commits intomainfrom
marandaneto wants to merge 4 commits intomainfrom
Conversation
Systematically optimize the posthog-js browser SDK to reduce transient memory allocations (GC pressure) by ~54% and minimize retained heap usage. Key optimizations: - Cache session props with session-change invalidation - Parse URL query string once instead of 20+ getQueryParam calls - Move before_send check before expensive deep copy - Eliminate eventProperties spread via single-pass property build - Bypass persistence.properties() by iterating props directly - Guard redundant per-capture persistence updates - Replace each()/eachArray()/entries() with direct for-loops - Hoist shared defaultConfig values (empty arrays, callbacks) - Skip defaultConfig() in constructor (replaced by _init) - Precompile search engine regex patterns - Reuse rate limiter result object - Add _hasCachedFlags() to avoid Object.keys array creation - Use Set for O(1) persistence reserved property lookups - Optimize configRenames and set_config to avoid unnecessary spreads
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
Contributor
|
Size Change: +34.6 kB (+0.53%) Total Size: 6.6 MB
ℹ️ View Unchanged
|
The error_tracking config property may be undefined when posthog is used as an uninitialized singleton (e.g. in React ErrorBoundary tests). Use optional chaining to prevent TypeError when accessing nested props.
Member
Author
|
next step here is to check the bundle size increase let agents to argue with each other for perf. x bundle size, add some sort of threshold so the experiment fails or not eg |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The posthog-js browser SDK creates significant transient memory allocations during every
capture()call and SDK initialization. These allocations increase GC pressure, which can cause jank and frame drops in performance-sensitive web applications. For customers capturing many events per page session, the cumulative GC cost is substantial.Changes
This PR systematically reduces transient memory allocations (GC pressure) by ~54% through ~30 targeted runtime optimizations across 10 source files. All changes are internal — no public API changes, no new dependencies, no breaking changes.
High-impact optimizations
getSessionProps()was called every capture, parsing URLs and campaign params each time. Now cached with session-change invalidation._getCampaignParamsFromUrlcalledgetQueryParam20+ times per URL, each splitting the URL/hash/query independently. Now parses once into a lookup.before_sendbefore deep copy_copyAndTruncateStrings(expensivedeepCircularCopy) now skipped entirely whenbefore_sendrejects the event.eventPropertiesspreadgetEventPropertiesbase instead of spreadingeventPropertiesfirst, then merging.Per-capture guard optimizations
update_search_keyword/update_referrer_info/set_initial_person_infoafter first call_requirePersonProcessingregister whenENABLE_PERSON_PROCESSINGalready seteventCapturedlogger withConfig.DEBUGcheck (avoids template literal + rest args){ ...propSet, ...dataSet }merge when no$setproperties existparamsToMaskarray whenmaskPersonalDataPropertiesis false (default)Data structure & iteration optimizations
SetforPERSISTENCE_RESERVED_PROPERTIESlookups — O(1) vs O(n) per property per capture_hasCachedFlags()— avoidObject.keys()array just to check flag existence (was called 2x per flag check)forloops replacingeach()/eachArray()/entries()throughout hot pathsextend()rewritten with directforloopsSimpleEventEmitter.emit()— cache array lookup, usesplice(indexOf)instead offilterextendArray()— directforloops instead ofeachArrayInit-time optimizations
defaultConfig()in constructor (70+ property object discarded by_init)defaultConfigvalues (empty arrays, callbacks, objects)set_config— use minimal comparison forupdate_configconfigRenamesto skip object creation when no renamed fields existsessionPersistencerecreation when persistence type unchangedstripEmptyPropertiescheck inupdate_campaign_paramspersistence.properties()— iterate props directly incalculateEventPropertiesDate.now()instead ofnew Date().getTime()in rate limiterOther
deepCircularCopy— pre-allocate arrays withnew Array(length)_copyAndTruncateStrings— skipslice()when string is within limitnormalizeFlagsResponse— single-pass loop replacingmap/filterchainparseFlagsResponse— avoid spread operator for conditional register propertiesextend({...})$snapshotpath readsdistinct_idwithout merged persistence objectsetPersonPropertiesForFlagsmerges into existing object directlyIntl.DateTimeFormat().resolvedOptions().timeZoneHow it was measured
A Jest-based memory benchmark creates a PostHog instance, captures 30 events, identifies a user, sets person properties, groups, receives 10 feature flags, checks all flags, and measures heap delta with
process.memoryUsage(). Ran 67 experiments across 6 sessions using an automated optimization loop.Release info Sub-libraries affected
Libraries affected
Checklist
If releasing new changes
pnpm changesetto generate a changeset file