Compatibility

Does obfuscation break islands and streaming SSR?

Islands and streaming server rendering both change where JavaScript comes from, which makes the compatibility question sharper than it is for a conventional single-bundle application. Some of the script on the finished page passed through your build and some of it was written by the server moments before it arrived, and a protection tool has a completely different relationship with each half. One consequence of that split breaks pages in a way no build check catches.

Two kinds of script arrive on the page, and only one came from your build

Islands architectures and streaming server rendering both change how JavaScript reaches the browser, which is why the question comes up. The useful way to think about it is to separate what the page receives into two categories, because a protection tool has a completely different relationship with each.

The first is your build output: the per-island client bundles, the chunks they import, the framework runtime, anything your bundler emitted into a client directory. All of that is ordinary JavaScript sitting in files, and it is exactly what a protector consumes. Nothing about the architecture changes that.

The second is script the server produced while responding to the request. Streaming frameworks emit small inline blocks as the response flows: bootstrap code that registers chunk loading, hydration payloads carrying serialised props, and a runtime fragment that resolves each suspended boundary as its data arrives. Those blocks did not exist when your build ran. No build step can transform something generated later, so they arrive as the framework wrote them.

Teams discover this by viewing source on a page they believed was fully protected and finding readable script. It is worth setting the expectation correctly in advance: that script is framework glue and data, not the application logic you were protecting.

Hydration payloads are data, and the distinction matters

The largest inline block on a streamed page is usually serialised state: props for each island, records fetched during rendering, the resolved output of server components. It travels inside the HTML document.

A JavaScript protector transforms programs. It has no relationship with a payload embedded in markup, and no string setting in your bundle reaches it. This is not a gap to be closed; it is a category difference, the same one that governs stylesheets, JSON responses and markup templates.

The practical consequence is worth stating plainly, because it is the genuine risk in this area and it has nothing to do with obfuscation. If something sensitive appears in a hydration payload, it has been published to anybody who views source, and the fix is to stop sending it rather than to protect the bundle harder. Internal identifiers, unfiltered records, pricing logic inputs and permission flags all end up there routinely, because it is easy to pass a whole object to a component that only renders two of its fields.

The failure mode that actually breaks these builds

Traditional applications import a module and call a function, and renaming is safe because both sides of every reference are rewritten together. These architectures introduce a pattern where that is not true.

A resumability-oriented framework, and to a lesser extent any framework that lazily attaches behaviour to server-rendered markup, records a reference during rendering. The server writes a symbol name into an attribute on the element. Later, when the user interacts, the runtime reads that attribute, loads the relevant chunk and resolves the name to a function.

The two halves of that reference are produced by different processes. The markup came from the server at request time. The chunk came from your build, which is where renaming happened. Rename the exported symbol and the attribute now points at a name that no longer exists, so the lookup fails and the component simply does not respond.

Three properties make this expensive. It does not fail at build time, because nothing is statically wrong. It does not fail on every component, only on the ones resolved by name. And it often does not fail in development, because development builds skip the protection step entirely, which is by design in several of the bundler plugins.

The fix is straightforward once the mechanism is understood: exclude the names the framework resolves at run time. Reserved names accepts regular expressions for names to preserve and maps them onto the variable exclusion list, so a pattern matching your framework's exported symbol convention keeps those stable while everything internal is still renamed. The work is in identifying them, and the place to look is the framework's own emitted markup rather than your source.

Many small bundles change the economics

An islands build replaces one large bundle with many small ones, and several things scale differently as a result.

  • Per-file overhead is paid once per island rather than once per application, so a helper preamble that was negligible in a single bundle becomes visible across forty of them.
  • Renaming consistency matters more, because islands that share a contract must agree on the names in that contract.
  • The framework runtime is usually shared, and protecting a public open-source runtime costs startup time while protecting nothing that was not already readable in its original repository.
  • Islands that never hydrate never pay their run-time transform cost at all, which is an advantage worth exploiting deliberately.

That last point is the most useful. Because most islands on a typical page are never activated, you can afford heavier settings on components that hydrate only on interaction, and should stay conservative on anything that runs immediately or blocks the first meaningful paint. Applying one profile to every island discards the main benefit of the architecture.

On the consistency question, the global renaming feature applies a single shared mapping across the files in one project. Islands protected together are internally consistent. Islands built by separate pipelines get separate mappings, and a name expected to match across that boundary will not, which is the same conclusion module federation arrives at from a different direction.

Where to put the protection step

After the framework build, against the client output directory. That is where the bundler plugins already operate, and the ordering is not arbitrary.

Running a protector over source before the framework processes it invites a specific class of problem: framework build steps analyse your code, recognising component boundaries, directives and export shapes in order to decide what to split, what to send to the client and what to keep on the server. Transformed source can defeat that analysis, and the resulting failure is confusing because it presents as a framework bug.

The plugin integrations encode the correct timing already. They hook a late optimisation stage in the bundler, after code splitting has settled, and several of them are inert during development builds unless explicitly enabled. If you are wiring this by hand, match that behaviour: build, then protect the emitted client assets, then deploy.

Testing has to account for arrival order

Streaming introduces variability that a fast local machine hides completely. Boundaries resolve in whatever order their data arrives, chunks load on their own schedule, and there is a genuine window during which markup is present but behaviour is not yet attached.

A protected build can behave differently in that window, because a name resolution failure or a startup cost only surfaces when the ordering is unfavourable. Throttle the connection. Force boundaries to resolve out of order. Interact with the page before hydration completes. Test against the production artifact rather than a development build, since that is the only configuration where the protection step actually ran.

None of this is exotic, and all of it is the kind of testing a streaming architecture warrants regardless of whether protection is involved. Protection just makes the consequences of skipping it more visible.

What the answer comes to

These architectures do not break under protection, and they do not protect themselves either. The client bundles are ordinary input and receive the full benefit. The inline streaming script and the hydration payload sit outside the tool's reach, which is a category difference rather than a shortcoming, and the sensible response is to keep anything valuable out of the payload for reasons that would apply anyway. The one real hazard is renaming a symbol the framework resolves by name at run time, and it has a direct fix that costs one configuration entry once you know which names are involved.

Frequently asked questions

Does an islands architecture change what a JavaScript protector can process?

Not the inputs, but it does change the shape of them. A protector works on JavaScript files, and an islands build still produces JavaScript files: one small bundle per interactive component instead of one large application bundle. Every one of those is ordinary input. What changes is that you now have many small entry points rather than a few large ones, which makes project-wide consistency matter more than it does in a monolithic build, and makes per-file overhead more noticeable because a fixed cost is paid more times.

What part of a streaming server-rendered page never reaches the protector?

The inline script blocks the framework emits while the response streams. Those are generated by the server at request time, containing hydration payloads, chunk-loading bootstraps and the small runtime that resolves suspended boundaries as they resolve. They do not exist when your build runs, so no build step can transform them. This is usually fine rather than alarming, because those blocks are framework glue and serialised data rather than anything you wrote, but it is worth knowing that a page you believe is fully protected will show readable script when you view source.

Is the hydration payload protected in any way?

No, and it is important to be clear that it is not code. Serialised props, fetched records and the resolved output of server components travel in the HTML document as data. A JavaScript protector transforms programs; it has nothing to do with a payload embedded in markup, and a string transform in your bundle does not reach it. The practical consequence is that anything sensitive appearing in a hydration payload is a data-exposure question to be solved by not sending it, exactly as it would be if it arrived from an API call.

What is the most common way protection genuinely breaks these architectures?

Name-based lookup at run time, which resumability-style frameworks depend on more heavily than traditional ones. When a framework records a symbol name in a markup attribute during rendering and later resolves that name against a chunk in order to attach behaviour, the recorded name and the shipped name have to agree. Renaming changes one side and not the other, because the markup was produced by the server and the chunk was produced by your build. The failure appears at interaction time rather than at build time, which is what makes it expensive to find.

How do we prevent that name mismatch?

By excluding the names the framework resolves at run time from renaming. The reserved names setting accepts regular expressions for names to preserve and maps them to the variable exclusion list, so a pattern covering your framework's exported symbol convention keeps those names stable while everything internal is still renamed. The prerequisite is knowing which names are looked up rather than called, which is worth determining from your framework's build output before the first protected deployment rather than after a bug report about a button that does nothing.

Does the framework runtime itself need protecting?

Almost never, and excluding it is usually the better call. The framework runtime is public open-source code that anybody can read in its original form, so transforming your copy protects nothing while adding startup cost and making stack traces harder to interpret. It also increases the chance of a collision with internal behaviour the framework relies on. Protect the code you wrote and the logic that is yours, and leave vendor runtime chunks out. Note that only two of the bundler plugins exclude a vendor directory by default, so on the others the exclusion is something you configure explicitly.

Do islands from separately built packages rename consistently?

Only within a project boundary, and that boundary is exactly what a multi-package setup crosses. The global renaming feature applies one shared mapping across the files in the same project, so anything protected in a single run is internally consistent. Two packages protected independently receive independent mappings, and a name that must match across them will not. If islands are built by separate pipelines and share a contract, keep the shared names out of renaming or use fixed custom identities for them, which is the same conclusion module federation reaches.

Does streaming change how we should test a protected build?

Yes, in one specific way that catches people. Streaming makes arrival order variable, so a protected build can behave differently depending on which boundary resolves first, which network conditions apply, and whether a chunk loads before the markup that needs it. Testing on a fast local machine hides all of that. Throttle the connection, test with boundaries resolving out of order, and exercise the page before hydration completes, because the window between markup arriving and behaviour attaching is where these problems live and it is invisible at full speed.

Should protection run before or after the framework's own build?

After, and against the client output directory, which is where the bundler plugins already put it. Running before means transforming source the framework will subsequently analyse, and framework build steps frequently depend on recognising their own patterns, so transformed source can defeat that analysis. Running against the emitted client bundles avoids the question entirely. The plugins also encode this timing: the bundler integrations hook a late optimisation stage and several of them are inert during development builds by design.

Does any of this apply to the server-rendered half?

It applies to server code only if you ship that code somewhere a customer runs it, and for most deployments you do not. Server-side rendering executes on infrastructure you control, so the confidentiality argument that motivates protecting a browser bundle is largely absent. The exception is self-hosted or on-premises delivery, where the customer runs the server, and there the server bundle deserves the same treatment as any distributed application code rather than the treatment given to a public front end.

Do partial hydration and lazy islands interact with startup cost?

They do, and the interaction is favourable if you configure it deliberately. The entire point of these architectures is that most components never hydrate, so the transform cost for a component that is never activated is never paid at run time. That means you can afford heavier settings on islands that hydrate on interaction, and should be conservative on anything that hydrates immediately or blocks the first meaningful paint. Treating every island identically wastes the main advantage of the architecture.

What is the shortest sensible checklist?

Six items. Protect the client output directory after the framework build, not the source before it. Exclude the framework runtime and vendor chunks. Identify every name the framework resolves at run time and reserve it. Keep all islands that share a contract inside one protection run, or pin the shared names. Accept that inline streaming script and hydration payloads are outside the protector's reach, and keep anything sensitive out of them. Then test throttled, with boundaries resolving out of order, against the production artifact.

Related reading