WebAssembly Feature Detection and Module Portability
Probe runtime capabilities, classify validate, compile, and instantiate failures, and deliver a fallback while preserving user input.
BotBrowser Team
Want the structured docs for Platform?
This article lives in the editorial library. For step-by-step setup, reference material, and ongoing updates, jump into the docs section.
Portability is a delivery decision made at runtime. A browser may expose the WebAssembly object yet reject the exact instructions, memory limits, or imports used by one module. A reliable application therefore probes the candidate artifact in the page where it will run, classifies the failing stage, selects a prepared fallback, and keeps the user's task intact. The unit being protected is the user's job, not an abstract browser score.
Runtime capability probe
Begin with a small, named capability record. Include the task identifier, module digest, required feature tier, import-contract version, and the page context. Check that the API exists, that the fetch returned the expected bytes, and that the page policy permits the intended worker or window. These observations are gates for the next operation, not a fingerprint. A probe should answer one product question, such as whether image-filter-v4-simd is eligible.
Run WebAssembly.validate(bytes) against the bytes that the selector would deliver. A true result means the engine accepts the binary structure and feature set; it says nothing about imports, allocation, or output correctness. A false result is useful evidence: mark that artifact unavailable in this context and move to a different tier. Do not retry identical bytes until a timeout changes, because validation is deterministic for a fixed engine and byte sequence.
Optional features deserve independent probes. SIMD, shared memory, bulk memory, and a large linear memory are not one capability. Give each probe a stable label and a short expiry. A runtime can accept SIMD while the page lacks the isolation required for shared memory. Keeping these facts separate lets the application choose a scalar module without disabling the entire task.
The probe runs after deployment conditions are established. A preview page, a worker, and a cross-origin-isolated product route can have different policy and import surfaces. Pass the same import names and configuration to the later instance. Distinguish an opaque fetch, a content-security-policy block, a network timeout, and a rejected binary. They all prevent the preferred path, but only one is evidence about engine support.
Keep capability telemetry minimal. Store the artifact id, stage, result, release label, and an optional user-reported incident id. Avoid navigator details and long timing traces by default. If support needs a record, retain it for the documented support period and delete it afterward. Capability data helps choose a path; it should not silently become a persistent identity profile.
Three failure stages
Use three named stages in both code and support records: validation, compilation, and instantiation. Return an application status such as { stage: 'instantiate', code: 'missing-import' } rather than displaying an engine's exception text as a contract. Error wording changes between releases; the stage and the application's code remain stable.
Validation failure means the bytes are malformed or request a feature unavailable to this engine. Imports have not been resolved yet. Typical responses are a baseline module, a JavaScript implementation, or a clear unsupported message. Keep the rejected digest in the release record so a later compiler change cannot be mistaken for a browser regression.
Compilation failure follows accepted validation. The engine may hit a resource limit, an implementation constraint, or a policy affecting executable-code generation. Do not label this as malformed input. Select a smaller or less aggressive artifact, or a non-WebAssembly path. Avoid a tight compilation loop; it can increase memory pressure without changing the condition.
Instantiation failure occurs when compiled code is connected to the host. A missing import, a wrong function signature, a table mismatch, or an allocation limit belongs here. Inspect the import adapter and the page context before changing support claims. If a controlled fixture succeeds while the product route fails, compare worker setup, policy, and host values rather than the browser version alone.
Execution errors happen after a successful instance and are a separate application outcome. An export may reject an input range, return a domain error, or exceed a task budget. Preserve the distinction: the capability is available even though this input needs an alternative. Stage-specific records let maintainers fix the right boundary and let users receive a useful message.
Fallback delivery
Ship fallback assets as first-class releases. A baseline WebAssembly module, a scalar variant, and a JavaScript path should each have an explicit input, output, and limitation contract. The selector evaluates the probe result and chooses the fastest path that satisfies the task, not simply the path with the most optional instructions.
Fallback selection is monotonic for a session. Remove an artifact after validation or compilation failure. If instantiation failed because an import map was incomplete, retry only with a known different map or a different artifact. This avoids duplicate work and makes telemetry explainable. Include feature tier and host-contract version in cache keys so a SIMD cache entry cannot satisfy a scalar request by accident.
The status interface should say which path is running and why. Explain that acceleration is unavailable, identify the selected alternative, and provide retry without clearing current input. A slower path is still a success when it produces the same contract. A reduced precision or smaller input limit must be visible before the user commits to it.
Test all branches with the same fixtures: valid fast artifact, validation rejection, compile pressure, missing import, successful baseline, cancellation, and retry. Assert output bytes or domain values, not just promise resolution. Run the selector in the real page context. BotBrowser documents JavaScript and WebAssembly parity for its baseline, Turbo, and SIMD pipelines; that documentation applies to those pipelines and is not a promise of identical output in every engine.
Preserve user input
Create a stable task record before probing. Keep the original file, text, or byte source available while selecting an implementation. A module that takes ownership of a buffer must receive a copy or a re-readable source; a failed candidate must never consume the only copy. Preserve encoding, ordering, and user edits so a fallback sees equivalent content.
Adapters translate from the stable task record into each module's alignment, pointer, or typed-array format. Translate the result back into the same application representation. Do not let a temporary module layout become the source of truth. On cancellation, mark the task cancelled, stop work where possible, release module-owned memory, and prevent a late promise from replacing the current view.
Privacy follows the data through every path. Local execution does not prove that inputs stay local: the page may fetch a module, send data to a service, or upload a result. State which fallback needs a network request and which works offline. Keep raw inputs out of capability logs. Accessibility also follows the transition: announce a path change, retain focus, and keep the input visible while a slower operation runs.
For adjacent operational guidance, see the browser release validation checklist and the API compatibility and feature fallback guide. Those guides complement the runtime decision; they do not replace it.
Fixed release record
Freeze a record before widening support. Include module and fallback digests, compiler and linker settings, feature tiers, import-contract version, fixture identifiers, page policy, browser profile, and stage results. Add the tested user task and the limitation shown to users. A release label makes the record searchable; a digest makes it auditable.
Keep functional evidence separate from timing evidence. Record representative inputs, output comparison, cancellation, and repeat use as correctness facts. Record latency and memory with workload and hardware context. When only the compiler changes, hold page and inputs fixed; when only the browser changes, hold artifacts and imports fixed. Changing every variable together produces an unexplainable green check.
Keep negative evidence. A record might say that the SIMD artifact was rejected at validation in the oldest profile, the scalar artifact instantiated, and both produced the expected output. Negative evidence prevents a future maintainer from deleting a fallback merely because a current browser accepts the fast path. Corrections receive a new immutable record with a reason and a link to the superseded entry.
Publish the task, preferred tier, fallback tier, tested range, cache-refresh instruction, and resume behavior. Do not claim universal browser support. The WebAssembly Core Specification, version 2 defines the binary and execution rules, while the MDN WebAssembly reference documents the JavaScript API. The fixed record connects those public rules to one observable delivery.
The operational sequence is short: probe in the real context; classify validation, compilation, and instantiation separately; choose a prepared fallback; preserve the task record and original input; then publish immutable artifact evidence. That sequence makes portability observable without pretending that the format erases browser or host differences.
The record names the exact response bytes, not only a URL, because a CDN can serve different content behind one address. It names the import adapter revision because an unchanged module can fail when its host function changes. It names the worker or window because a worker can expose a different lifecycle and policy. It names the input class because a ten-byte fixture cannot prove that a ten-megabyte upload fits the memory budget.
It records whether validation was run on the fetched response or on a bundled copy. It records the compile outcome independently from the time spent compiling. It records instance creation independently from the first export call. It records whether an output was committed, displayed as provisional, or discarded after cancellation. These distinctions make a support conversation concrete: an operator can ask which stage failed instead of asking whether WebAssembly works.
It records the fallback reason using application vocabulary: feature-missing, policy-blocked, resource-limit, host-contract, or network-unavailable. The vocabulary is intentionally small. A new reason requires a contract update and a test. Free-form engine messages remain debugging material and are not used to select a product path. This keeps behavior stable across browser updates and locales.
It records the user's ability to resume. A paused task retains its source and selected tier; a cancelled task can be restarted deliberately; a committed result has a module digest attached. If an offline cache is refreshed, the record explains whether an in-progress task finishes with the old artifact or waits for the new one. The choice is part of product behavior, not an incidental cache detail.
It records a privacy decision: what was retained, for how long, and which service received data. A local module can still be surrounded by network activity. A server fallback can be correct while requiring consent or a different retention notice. The release evidence therefore includes the data path without collecting the data itself. This is how a capability check stays useful without becoming surveillance.
It records accessibility observations for each branch. The fast path, fallback, and no-path message must all expose a readable status and preserve keyboard access. Progress labels identify validation, compilation, or processing rather than using a single indefinite spinner. Screen-reader output and visible text describe the same selected path. A release is incomplete if only a sighted developer can tell why a fallback appeared.
It records repeat use. Run a second task after the first completes, then run a cancellation followed by a retry. Memory ownership bugs and stale imports often appear only on the second instance. Include a worker restart where the product uses workers. Include a page reload where a service worker owns the cache. These tests turn lifecycle assumptions into evidence.
It records the oldest supported baseline and the candidate release. Test the preferred artifact and every advertised fallback in both. If the baseline cannot run the preferred artifact, the record must show the selected alternative rather than marking the whole feature failed. If the candidate changes a result, reduce the input and identify whether the difference belongs to the specification, the compiler, or the host adapter.
It records source URLs used for normative and API claims. The public sources for this article are the W3C Core specification, MDN WebAssembly, and BotBrowser advanced features. Source links are not a substitute for product tests; they explain terms and documented capabilities. The release record links the test evidence that demonstrates the chosen path under the application's actual deployment policy.
It records the publication date and the person or system that approved the support range. A later edit creates a new record rather than changing the old one in place. This preserves the reason a fallback existed when a user report arrives months later. It also lets a release manager compare decisions across compiler upgrades without relying on memory or a mutable dashboard.
It records what was intentionally not tested. For example, a report may exclude a private browser build, an unsupported worker mode, or an input larger than the service budget. Clear boundaries are safer than implied promises. The product page can then say “tested for these tasks and profiles” instead of suggesting that every host with a WebAssembly object has identical behavior.
It records the final user-facing copy for a fallback and the localization key used to render it. Translators can preserve the meaning of a stage and limitation without copying an engine exception. A language-specific message still points to the same task state and recovery action. This keeps the five locale articles and the product interface semantically aligned while allowing natural wording.
It records the removal plan for obsolete artifacts. Retire a module only after active tasks, offline caches, and documented rollback windows are accounted for. Keep the digest in historical records even when the bytes are no longer served. A missing old URL should not make a release explanation impossible. Artifact lifecycle is part of module portability because users may cross a deployment boundary during a task.
It records the final decision as a small, inspectable object: selected tier, reason, stage if a preferred tier failed, input retention state, and release label. Everything else is supporting evidence. This object is suitable for a support report, a deterministic test assertion, and a user-visible status translation. It is deliberately less detailed than a browser fingerprint and more useful than one.
Finally, rehearse the decision with the team that owns the page. Ask an engineer to remove an import, a tester to serve a truncated response, and a support specialist to resume a cancelled upload. The expected outcome is not merely a green test: each person should be able to identify the stage, see the preserved input, and explain the selected path. A portability contract is valuable when it remains understandable during an incident, when the browser, network, and application all change at once.
Keep that rehearsal close to the release record. When a new compiler or browser changes a result, rerun the smallest failing fixture first, then the full task matrix. Compare the immutable artifact and host-contract labels before changing code. This order keeps investigation focused and prevents a local workaround from silently changing the fallback promised to users. The goal is predictable recovery, not a claim that every runtime behaves identically.
Public sources
Related Articles
Take BotBrowser from research to production
The guides cover the model first, then move into cross-platform validation, isolated contexts, and scale-ready browser deployment.