Skip to content

Add mixpanel-browser/core and mixpanel-browser/async-modules entrypoints (-72% gzipped) - #652

Open
Mohith26 wants to merge 1 commit into
mixpanel:masterfrom
Mohith26:feat/slim-entrypoints
Open

Mohith26 wants to merge 1 commit into
mixpanel:masterfrom
Mohith26:feat/slim-entrypoints

Conversation

@Mohith26

@Mohith26 Mohith26 commented Nov 4, 2026 •

Copy link
Copy Markdown

Refs #482.

The SDK already builds bundles without the session recorder (dist/mixpanel-core.cjs.js, dist/mixpanel-with-async-modules.cjs.js), but the only documented way to use them is a deep import into src/loaders/.... That path is raw source that every app re-bundles, it is awkward for newer bundlers (see the Turbopack comments in #482), and the async variant currently has a bug (below). This PR makes the slim bundles first-class:

import mixpanel from 'mixpanel-browser/core';          // no recorder
import mixpanel from 'mixpanel-browser/async-modules'; // recorder/targeting loaded from the CDN on demand

Changes

  • Build ES module versions of the core and async-modules bundles (dist/mixpanel-core.module.js, dist/mixpanel-with-async-modules.module.js); copyTypes() writes their .d.ts files.
  • Add core/ and async-modules/ directories with a small package.json (main, module, types) pointing at those bundles, and add them to files. I deliberately did not add an exports map, since that broke existing deep imports in 2.69.0; everything that resolves today (src/..., dist/...) still resolves.
  • Fix: when the SDK is bundled straight from src/ (the README's loader-module-with-async-modules import), the __MP_RECORDER_FILENAME__ / __MP_TARGETING_FILENAME__ placeholders are never replaced, so session replay requests https://cdn.mxpnl.com/libs/__MP_RECORDER_FILENAME__ (404). It now falls back to the unhashed mixpanel-recorder.min.js / mixpanel-targeting.min.js, which is what those imports did before the hashed bundles landed. Built bundles are unaffected and still get the hashed names.
  • README now points at the new entrypoints.
  • npm run bundle-size bundles a minimal app (init + track) against each entrypoint with rollup + esbuild minify and prints the sizes.

Bundle size (npm run bundle-size after a full build, Apple Silicon arm64; sizes are deterministic, same output on 3 runs)

import minified gzipped vs. default (gzipped)
mixpanel-browser 427.7 KiB 128.4 KiB
mixpanel-browser/async-modules 124.5 KiB 35.6 KiB -72%
mixpanel-browser/core 124.4 KiB 35.6 KiB -72%

Testing

  • npm test (lint + 923 unit tests) passes. New tests/unit/entrypoints.js checks that each entrypoint is published, that main/module/types point at outputs the rollup config actually produces, that the CJS bundle loads, and the src/ filename fallback.
  • Ran a full build (FULL=1 rollup -c), then npm pack and installed the tarball into a scratch project: require('mixpanel-browser/core') and /async-modules work in Node, esbuild picks the new .module.js files, and tsc --strict resolves real types for both subpaths with moduleResolution node10, node16 and bundler.
  • Before my change, the same build reproduced the committed dist/ byte-for-byte, so the build setup matches. I did not commit dist/ since it is generated at release time; the new .module.js files will appear on the next npm run build-dist.

This doesn't shrink the default import, so it doesn't fully close #482, but it gives everyone who doesn't bundle session replay a supported way to drop about 93 KiB gzipped.

I used AI assistance for this PR and reviewed and tested the changes myself.

Exposes the prebuilt bundles without the session recorder as proper package
subpaths with ESM, CJS and TypeScript entrypoints, so apps that don't need a
bundled recorder can drop ~72% of the SDK's gzipped size without deep imports
into src/ or dist/.

- build ESM versions of the core and async-modules bundles
- add core/ and async-modules/ directory entrypoints (no exports map, so all
  existing deep imports keep working)
- fall back to the unhashed CDN bundles when the SDK is bundled straight from
  src/, where the async module filename placeholders are never replaced
- add npm run bundle-size to measure each entrypoint

Refs mixpanel#482
@Mohith26
Mohith26 requested review from a team and tdumitrescu November 4, 2026 17:36
@jakewski

jakewski commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

Hey @Mohith26 thanks for the contribution! Can your problem be effectively solved by importing from dist instead of loaders?

// Core SDK, no session recording
import mixpanel from 'mixpanel-browser/dist/mixpanel-core.cjs.js';

// Core SDK; session recording and targeting load from the CDN when needed
import mixpanel from 'mixpanel-browser/dist/mixpanel-with-async-modules.cjs.js';

We should definitely update our README to indicate this, we could also add export entries for shorter names as a simpler convenient approach

@Mohith26

Mohith26 commented Nov 7, 2026

Copy link
Copy Markdown
Author

Thanks for taking a look. I tried it, and yes, importing from dist solves the main problem.

  • I bundled a minimal app (init + track) with esbuild against 2.85.0: mixpanel-browser is 433 KiB minified / 129.5 KiB gzipped, and both dist/mixpanel-core.cjs.js and dist/mixpanel-with-async-modules.cjs.js come out at 135 KiB / 38.2 KiB. That's about 70% smaller, close to what the ESM builds in this PR get.
  • I ran both bundles in a browser page. Core inits and tracks, and the async build loads mixpanel-recorder-KeINni5H.js from cdn.mxpnl.com when recording starts and the recorder initializes. Types resolve too since the .cjs.d.ts files ship next to the bundles.
  • The __MP_RECORDER_FILENAME__ 404 only happens on the src/loaders imports, so pointing the README at dist sidesteps it.

So the extra ESM builds and the core/ / async-modules/ directories aren't really needed. I'm happy to pivot this PR to:

  1. README: document the two dist imports instead of src/loaders.
  2. exports entries for mixpanel-browser/core and mixpanel-browser/async-modules, plus ".", "./package.json" and a "./*" passthrough so existing dist/... and src/... imports keep resolving.

One thing to flag on 2, given what happened in 2.69.0: once exports exists, Node no longer adds extensions, so the extensionless mixpanel-browser/src/loaders/loader-module-core path the README documents today needs its own entries to keep working, and TypeScript with moduleResolution: node10 ignores exports, so the short names would only type-check on node16/bundler. I'd add explicit entries for the existing loader paths and test the old imports against a packed tarball. Does that work for you, or would you rather keep this PR to the README change?

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.

Bundle size of mixpanel-browser

2 participants