This PR reduces the web SDK's gzipped bundle size by about 30%, lowering
the amount of JavaScript downloaded and parsed on uncached SDK loads. It
also serves precompressed Brotli or gzip from the existing SDK URL and
adds repeatable bundle-analysis tooling to help catch future size
regressions.
## What changed
### Use a lightweight synchronous user fingerprint
The SDK used MD5 only as a local fingerprint for detecting whether
`setUser` data had changed. It was not used as a conversation
identifier, authentication mechanism, or part of the server API
contract. This PR replaces the MD5 dependency with a small inline
128-bit [FNV-1a](https://www.rfc-editor.org/rfc/rfc9923.html)
implementation.
FNV-1a is synchronous and works in both secure and insecure embedding
pages. This preserves the previous latest-call-wins behavior when
integrations call `setUser` repeatedly, without relying on
`SubtleCrypto`, which is unavailable on plain HTTP origins. The 128-bit
output prevents practical 32-bit collisions from causing distinct
identities to be treated as unchanged; regression coverage includes a
known pair of identifiers that collide under FNV-1a 32-bit. Existing
hash cookies produce a one-time mismatch, causing `setUser` to be sent
once before the cookie is replaced with the new fingerprint.
The ESLint environment now declares ES2020, matching the SDK's Vite
build target and allowing the implementation to use `BigInt` directly.
The SDK also used `date-fns` only to add one hour to the current time.
Native timestamp arithmetic provides the same behavior for this fixed
duration without bundling the associated date utilities.
### Minify inline SDK styles
SDK styles now pass through [Vite's inline CSS
pipeline](https://v6.vite.dev/guide/features#disabling-css-injection-into-the-page).
Vite minifies the stylesheet at build time and returns it as a string,
while the SDK continues injecting it into the embedding page as before.
### Serve precompressed bundles from the same URL
The SDK build now emits Brotli and gzip sidecars. Rails 7.2
[automatically negotiates precompressed
assets](https://api.rubyonrails.org/v7.2.3.1/classes/ActionDispatch/FileHandler.html)
and serves the best format supported by the browser from the existing
`/packs/js/sdk.js` URL. No embed-code or application URL changes are
required.
## Bundle size
| Metric | Before | After | Reduction |
|---|---:|---:|---:|
| Raw | 29.20 kB | 21.75 kB | 25.5% |
| Gzip | 9.73 kB | 6.89 kB | 29.2% |
| Brotli | — | 6.08 kB | — |
## How to test
1. Load the widget from `/packs/js/sdk.js` in a browser that supports
Brotli or gzip and confirm the same URL is returned with the
corresponding `Content-Encoding` header.
2. Embed the widget on HTTPS and plain HTTP pages, call
`$chatwoot.setUser`, and confirm the user is identified in both
contexts.
3. Call `setUser` several times consecutively with different identities
and confirm the final call remains the active widget identity.
4. Reload the embedding page with unchanged user data and confirm the
SDK does not send a redundant `setUser` update.
5. Confirm the widget layout and styles remain unchanged.
## Staging test
#### Before
<img width="2192" height="710" alt="CleanShot 2026-08-12 at 16 23 27@2x"
src="https://github.com/user-attachments/assets/b60cf6dc-bb12-460c-9bed-d8720010ccf6"
/>
#### After
<img width="2192" height="1014" alt="CleanShot 2026-08-12 at 16 57
53@2x"
src="https://github.com/user-attachments/assets/4ce97fab-35a8-44cc-8add-20a0d59d5755"
/>