Skip to content

Stream: restore noise cancellation and background blur, and put a size budget on the server function #1158

Description

@teetangh

Parked from #1143, which was closed after failing to deploy 18 times out of 18. The work is preserved intact on parked/stream-call-filters at c1e574a2. Part of #1134.

This issue carries three things: what was built, why it was parked, and a standing risk that is larger than the feature itself.

The standing risk, first — because it outlives this feature

The operative limit is AWS Lambda's 250 MB unzipped package size (AWS quotas), which Netlify surfaces as Invalid AWS Lambda parameters used in this request on ___netlify-server-handler. It is not raisable. Vercel now offers functions up to 5 GB behind a flag; Netlify has no equivalent escape hatch.

The important correction is that the parked package was not 44 MB of lambda weight. Measured against this repo:

Path Size Reaches the server lambda
src/krispai/ (Krisp models) 33 MB No. Referenced only through a runtime URL template — `unpkg.com/${packageName}@${packageVersion}/src/krispai/models` — with zero filesystem requires, so @vercel/nft has nothing to trace.
dist/cjs/index.js.map 5.5 MB No. Next's serverIgnores includes **/*.map.
dist/cjs/index.js 5.4 MB Yes. Statically imported from a "use client" component, so webpack inlines it into the SSR-layer server chunks.
dist/types/ 44 KB No, erased at build.

So roughly 5.4 MB tipped the deploy over. Which means the server function was already sitting near 245 MB, and this package was the last straw rather than the cause. The next dependency of any size will break the deploy again, and it will present exactly as this one did — every required check green, next build succeeding locally, and a failure only at the deploy stage.

That deserves its own attention regardless of whether noise cancellation is ever restored. node_modules here is about 1.5 GB, with @prisma at 164 MB and @sentry at 78 MB. The durable fix is a size budget on the handler, measured in CI, not a special case for one package.

The measurement to start from, which is also Netlify support's own recommended diagnostic:

netlify build
du -sh .netlify/functions-internal/___netlify-server-handler
du -sh .netlify/functions-internal/___netlify-server-handler/node_modules/* | sort -rh | head -30

Why the two fixes attempted during triage did not work

Both are recorded here because they are on the parked branch and both are no-ops. Anyone restoring that branch will otherwise assume they were merely misconfigured.

netlify.toml [functions] included_files = ["!…"] cannot shrink the Next.js server handler. The Netlify adapter writes a per-function config file containing includedFiles: ['**'] and nodeBundler: 'none', and zip-it-and-ship-it merges that file over your netlify.toml with a shallow spread (adapter source, zisi config). Because node_bundler ends up none, external_node_modules is inert too — Netlify documents it as applying only when the bundler is esbuild. The ! remedy is real, but it only appears in Netlify's legacy runtime v4 troubleshooting page, which carries a maintenance-support banner for Next.js 10–13.4.

outputFileTracingExcludes only prunes trace-delivered bytes. It cannot remove code webpack has already inlined into .next/server/chunks/*, and in the App Router external packages are bundled by default. Since the package is statically imported from a "use client" component, its bytes are inlined and no tracing exclude reaches them. Two further traps worth knowing if it is used elsewhere: the matcher runs with { contains: true }, so patterns match substrings and can silently delete unrelated paths (vercel/next.js#62331, still open); and Turbopack builds skip the mechanism entirely (vercel/next.js#77656, still open), which becomes relevant the moment this repo moves to Next 16 or passes --turbopack.

Moving the package to devDependencies would also be a placebo. Netlify sets no NODE_ENV by default, so devDependencies are installed, the import resolves, and webpack bundles it identically.

What would actually work

next/dynamic(..., { ssr: false }) around the noise-cancellation provider, called from inside a "use client" file. Next's SWC transform rewrites the loader to require.resolveWeak(...) on the server compilation, which resolves an id without creating a chunk dependency, so the module is not emitted into the SSR-layer output. This is the only approach among those considered that provably removes the 5.4 MB, and it fits the existing design, which already does not mount the provider by default.

Note that "use client" alone does not achieve this — client components are server-rendered by default in the App Router, which is precisely the trap that produced this bug.

What is already built, on parked/stream-call-filters

Restoring this is a rebase and a checkout, not a rewrite. The branch is based on an older dev and must be rebased first — much of its apparent diff is unrelated chat and script work that has since merged.

File What it does
app/meetings/[id]/components/NoiseCancellationGate.tsx Owns the Krisp provider. Deliberately mounted outside the dropdown, because Radix unmounts closed menu content and would tear the provider down.
app/meetings/[id]/components/CallFiltersProvider.tsx Wraps both the lobby and the room, because the filter registers on call.camera and a provider unmounting between them would unregister it exactly as the person joins.
app/meetings/[id]/components/CallEffectsMenu.tsx, EffectRow.tsx The in-call effects UI.
scripts/copy-stream-filter-assets.mjs Copies the WASM and model files out of node_modules into public/ at postinstall, so they are served from our own origin.
next.config.mjs CSP changes worker-src pinned rather than inherited, wasm-unsafe-eval and blob: added to script-src.

The self-hosting is worth keeping exactly as it is. public/ is copied to Netlify's static output, not into the lambda, so it costs zero function bytes — and it matches Stream's own documented mechanism, which otherwise fetches models from unpkg.com at runtime from inside a live consultation. That would put a third party on the call path and require opening connect-src to them.

One review comment from #1143 is unaddressed and belongs to this work: dropdown triggers in CallEffectsMenu.tsx omit an explicit type attribute and rely on Radix forwarding it.

The product question

Noise cancellation via Krisp is a paid Stream add-on billed per participant-minute. That is a recurring cost on a pre-MVP product for a feature nobody has asked for, and the billing shape is awkward on its own terms: Stream meters the whole call, but the feature is toggled by a single participant, so it is unclear who should pay when a consultee switches it on. Charging for it would need per-call metering, reconciliation against Stream's invoice, and a dispute path — disproportionate machinery for an audio nicety.

I was unable to verify Stream's current published pricing for this add-on. The research pass that was to confirm the exact unit and whether it bills the whole call or only the enabling participant did not complete. That should be confirmed before any pricing decision, and it is the crux of the deferral rather than a detail.

Background blur is a separate, cheaper case

@stream-io/video-filters-web arrives as a transitive dependency of @stream-io/video-react-sdk, so it is installed on dev regardless and dev deploys fine. Restoring blur adds no package that is not already present. It is not paid. It is a much better candidate for near-term work than noise cancellation, subject to the same ssr: false treatment and the same size budget.

Suggested order

  1. Measure the server function and establish a size budget checked in CI. This is worth doing whether or not either feature returns, because the next dependency will otherwise reproduce this failure.
  2. Restore background blur with next/dynamic(..., { ssr: false }), since it costs no new dependency.
  3. Revisit noise cancellation post-MVP, once Stream's billing model is confirmed and there is a pricing structure that can absorb it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    launch: post-mvpFirst 90 days after launch — coverage, polish, operational maturity

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions