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" />
39 lines
990 B
TypeScript
39 lines
990 B
TypeScript
import path from 'path';
|
|
import yaml from '@rollup/plugin-yaml';
|
|
import vue from '@vitejs/plugin-vue';
|
|
import { defineConfig, type PluginOption } from 'vite';
|
|
import { visualizer } from 'rollup-plugin-visualizer';
|
|
import { aliases, vueOptions } from './vite.shared';
|
|
|
|
export default defineConfig({
|
|
plugins: [
|
|
vue(vueOptions),
|
|
yaml(),
|
|
visualizer({
|
|
filename: path.resolve(__dirname, 'tmp/bundle-analysis/widget.html'),
|
|
title: 'Chatwoot widget bundle analysis',
|
|
template: 'treemap',
|
|
gzipSize: true,
|
|
brotliSize: true,
|
|
projectRoot: __dirname,
|
|
}) as PluginOption,
|
|
],
|
|
css: {
|
|
preprocessorOptions: {
|
|
scss: {
|
|
api: 'modern-compiler',
|
|
},
|
|
},
|
|
},
|
|
publicDir: false,
|
|
build: {
|
|
outDir: 'tmp/bundle-analysis/widget',
|
|
emptyOutDir: true,
|
|
copyPublicDir: false,
|
|
rollupOptions: {
|
|
input: path.resolve(__dirname, './app/javascript/entrypoints/widget.js'),
|
|
},
|
|
},
|
|
resolve: { alias: aliases },
|
|
});
|