Split Prebid into deferred bundle to reduce render-blocking JS#393
Open
Split Prebid into deferred bundle to reduce render-blocking JS#393
Conversation
Prebid.js (168 KB) was 80% of the TSJS unified bundle, blocking page
rendering. Split it into a separate `<script defer>` tag so the
critical-path bundle drops from 190 KB to 22 KB.
Changes across 8 files:
- crates/js/src/bundle.rs: add `single_module_hash()` and
`all_module_ids_excluding()` for deferred module serving
- crates/js/src/lib.rs: export new bundle functions
- crates/common/src/tsjs.rs: add `DEFERRED_MODULE_IDS` constant (single
source of truth), deferred script tag generation functions, update
`_all()` variants to exclude deferred modules
- crates/common/src/integrations/registry.rs: add
`js_module_ids_immediate()` and `js_module_ids_deferred()` with tests
- crates/common/src/html_processor.rs: inject split tags at `<head>`:
synchronous main bundle + deferred prebid tag
- crates/common/src/publisher.rs: serve deferred modules at
`/static/tsjs=tsjs-{id}.min.js` with allowlist validation and tests
- crates/common/src/integrations/prebid.rs: fix test assertion for new
deferred tag presence
- docs/guide/integration-guide.md: update Prebid load timing docs
Closes #358
Update CLAUDE.md and integration-guide.md to indicate which integrations use deferred loading and explain the two loading modes.
Each integration now declares deferred JS loading via .with_deferred_js() on IntegrationRegistrationBuilder, replacing the static constant that required manual maintenance. Removes unused _all() convenience functions from tsjs.rs and all_module_ids_excluding() from bundle.rs.
tsjs_script_src(&["testlight"]) hashed only core+testlight, but the unified bundle contains all immediate modules. Added tsjs_unified_script_src() and tsjs_unified_script_tag() that hash all module IDs so the cache key matches the actual served content.
prk-Jr
requested changes
Mar 4, 2026
Collaborator
There was a problem hiding this comment.
Summary
This PR achieves a strong perf win by splitting Prebid out of the render-blocking bundle. I found one high-severity compatibility risk with deferred loading and two low-priority cleanup items.
Findings
🔧 High (P1)
- Deferred Prebid can break pages with bare
pbjscalls: The HTML rewrite removes publisher Prebid script tags and loads Prebid via<script defer>, but there is no earlywindow.pbjsbootstrap. Inline scripts that callpbjs.que.push(...)without first initializingpbjscan throw before deferred Prebid executes.
⛏ Low (P3)
- Docs inconsistency after deferred split:
docs/guide/integration-guide.mdstill has earlier sections describing Prebid as part of the unified sync bundle and describing head insert ordering that no longer matches implementation.
CI Status
- cargo fmt: PASS
- cargo test: PASS
- vitest: PASS
Inject window.pbjs, pbjs.que, and pbjs.cmd in the head injector inline script so publisher pages with bare pbjs.que.push() calls don't throw before the deferred bundle executes. Also fix stale doc references that still described Prebid as part of the unified sync bundle.
ChristianPavilonis
approved these changes
Mar 5, 2026
Collaborator
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Looks pretty good.
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.
Summary
Closes #358
Prebid.js (168 KB raw) was 80% of the TSJS unified bundle, served as a single render-blocking
<script>in<head>. This splits it into a separate<script defer>tag so the critical-path bundle drops from 190 KB to 22 KB.Performance results (local dev environment, Chrome DevTools trace, no network throttling)
Performance results (staging vs production, p95 over 10 runs, Chrome 1440x900)
Staging serves the split bundle: 46 KB blocking (unified) + 182 KB non-blocking (prebid deferred). Production serves a single 228 KB blocking bundle.
Metric definitions
DOMContentLoadedcan now run.Why this is safe
build-all.mjswindow.__tsjs_prebid(injected before it via inline script)deferpreserves document order — inline config runs first, then main bundle, then prebidpbjs.quequeue pattern, so commands are queued until prebid loadsWhat changed
Split Prebid into deferred bundle (8 files, Rust-only — no JS or build changes)
crates/js/src/bundle.rs— Addedsingle_module_hash()for per-module cache busting.crates/js/src/lib.rs— Exported new functions.crates/common/src/tsjs.rs— Added deferred script tag helpers (tsjs_deferred_script_src(),tsjs_deferred_script_tag(),tsjs_deferred_script_tags()).crates/common/src/integrations/registry.rs— Addedjs_module_ids_immediate()andjs_module_ids_deferred()with tests.crates/common/src/html_processor.rs— Updated<head>injection to emit main bundle + deferred tags separately.crates/common/src/publisher.rs— Updatedhandle_tsjs_dynamicto serve both unified and deferred bundles with allowlist validation.crates/common/src/integrations/prebid.rs— Updated test assertions for deferred tag.docs/guide/integration-guide.md— Updated Prebid load timing documentation.Replace hardcoded constant with per-integration builder flag (10 files)
Replaced the
DEFERRED_MODULE_IDSconstant intsjs.rswith a.with_deferred_js()method onIntegrationRegistrationBuilder. Each integration now declares its own loading mode instead of maintaining a central list.crates/common/src/integrations/registry.rs— Addedjs_deferredfield toIntegrationRegistration,.with_deferred_js()builder method, anddeferred_js_idsstorage in registry. Updatedjs_module_ids_immediate()/js_module_ids_deferred()to use it.crates/common/src/integrations/prebid.rs— Added.with_deferred_js()to registration.crates/common/src/tsjs.rs— RemovedDEFERRED_MODULE_IDSconstant and_all()convenience functions.crates/common/src/creative.rs— Uses explicit module list&["creative"]instead of_all().crates/common/src/integrations/testlight.rs— Uses explicit&["testlight"]instead of_all().crates/common/src/publisher.rs—parse_deferred_module_filenamevalidates againstall_module_ids()(deferred check in caller).crates/js/src/bundle.rs— Removedall_module_ids_excluding().crates/js/src/lib.rs— Removed export.CLAUDE.md,docs/guide/integration-guide.md— Updated docs.Fix cache-busting hash for unified bundle (3 files)
crates/common/src/tsjs.rs— Addedtsjs_unified_script_src()andtsjs_unified_script_tag()that hash all immediate module IDs (not just a subset).crates/common/src/creative.rs— Usestsjs_unified_script_tag().crates/common/src/integrations/testlight.rs— Usestsjs_unified_script_src().Resulting HTML output
When prebid is enabled:
When prebid is disabled, no deferred tag is injected.
Test plan
cargo test --workspace— all 424 tests passcargo clippy --all-targets --all-features -- -D warningscargo fmt --all -- --check/auctionorchestrator