2026-09-02 · Updated 2026-09-10 · 10 min read · CanWeChat Engineering
Why the CanWeChat widget is ~10 KB and not a React app
Host pages should not download your dashboard. CanWeChat’s public widget is vanilla TypeScript in a Shadow DOM, not a React island.
If you run a marketing site or SaaS landing page, every kilobyte on the critical path competes with your own product. Live chat vendors often solve their engineering convenience first: ship a miniature SPA, hydrate a framework, and hope Core Web Vitals hold. We designed CanWeChat the other way around.
This article is for founders and engineers evaluating lightweight live chat widgets—especially anyone who has watched a third-party script push LCP or CLS after “just adding chat.” After reading, you should know what lives in the public widget, what stays in the dashboard, and when a small vanilla bundle is the right tradeoff.
Quick answer
- The public CanWeChat surface is a minified vanilla TypeScript IIFE, not a React (or any framework) island on customer pages.
- Styles and DOM live in Shadow DOM so host CSS and widget CSS do not fight.
- A stub queues CanWeChat.identify / open / prefill calls until the runtime boots.
- Realtime, auth, billing, and reply-channel admin stay on Cloudflare Workers, D1, and Durable Objects—behind your dashboard session.
- Aim for one site per workspace and one active reply destination so the widget stays a messenger, not a helpdesk suite.
What most chat widgets actually ship
A full dashboard needs rich UI: conversation lists, filters, member invites, billing portals, integration forms. That is a fair job for React or Vinext. The mistake is reusing that stack as the visitor-facing embed. Once you do, every marketing page downloads scheduler code, reconciliation, and often a second copy of design-system CSS that was never meant for anonymous traffic.
That cost shows up as: longer parse time on mid-tier phones, style collisions with your theme, and a larger attack surface on a public origin. “It works on our MacBook” is not the same as “it is cheap on your customer’s blog.”
Design constraints we refused to break
- No framework runtime on host pages. React stays in the CanWeChat dashboard.
- CSS isolation by default. Shadow DOM, not a global stylesheet dump.
- Async load with a callable stub so identify() after login does not race the script.
- Domain locking via allowed hosts so a public site key is not a free-for-all embed token.
- Narrow product scope: one website, one active reply destination (inbox, Slack, Discord, or Telegram).
How the loader works
Install is intentionally boring. You add one async script with your public site key on data-site. The loader reads that attribute, establishes an API base, and boots the UI into a shadow root. Until boot finishes, a stub implements the same method names and queues calls.
<script
async
src="https://canwechat.dev/api/widget.js"
data-site="pk_live_YOUR_KEY"
></script>After your app authenticates a user, call identify so agents see a stable visitor instead of an anonymous bubble:
window.canwechat?.identify({
userId: user.id,
email: user.email,
name: user.name,
});
// Optional: open from a pricing CTA
document.querySelector("#ask-sales")?.addEventListener("click", () => {
window.canwechat?.open();
});Shadow DOM isolation
Host sites use Tailwind, Bootstrap, CSS modules, or decade-old global resets. A chat bubble that relies on unscoped class names will eventually inherit a 14px line-height or a button { all: unset }. Shadow DOM keeps widget styles local. You still control brand colors and copy from the CanWeChat dashboard; you do not inherit accidental host rules.
What stays off the page
- Better Auth email OTP and session cookies for the dashboard.
- Dodo Payments checkout and entitlement updates (webhook-driven, never redirect-trusted).
- Encrypted Slack / Discord / Telegram credentials and signing secrets.
- Outbound webhook relay admin, delivery logs, and queue retries.
- Conversation search, team settings, and billing portal UI.
Edge architecture behind the bubble
Visitor messages hit Cloudflare Workers. Conversation history lands in D1. Realtime sockets use Durable Objects so inbox and widget stay in sync without turning the embed into a polling client. Integration fan-out (Slack posts, Discord messages, Telegram sends, signed customer webhooks) goes through queues so a slow third-party API does not block the visitor’s send path.
That split matters for performance storytelling: the small widget is not “magic.” It is the public tip of a system that deliberately keeps heavy work off the marketing origin.
Checklist before you embed any chat script
- Measure LCP and INP on a mid-tier mobile profile before and after adding the script.
- Confirm the vendor isolates CSS (Shadow DOM or equivalent).
- Confirm you can lock allowed domains for the public key.
- Confirm identify() (or equivalent) exists so logged-in users are not anonymous forever.
- Refuse embeds that require a React provider tree on every marketing route “for convenience.”
- Decide one reply destination your team will actually watch.
The CanWeChat angle
CanWeChat is priced and scoped for small teams: a 3-day free trial, then $19/month for one site and one active reply destination. The widget’s job is to collect the question and carry the reply—not to become a second helpdesk your team abandons. Docs for install and the Widget API stay public; dashboard session routes are not a partner API.
Limitations (read these before you choose us)
- If you need multi-brand suites, complex routing trees, or AI knowledge-base agents as the product, buy a helpdesk platform—not a narrow messenger.
- One active reply destination means Slack and Discord are not both “live” at once by design.
- We do not claim SOC 2, ISO, or a measured public SLA on this page. Judge us on domain lock, signed webhooks, encrypted secrets, and how we handle billing entitlement.
- Bundle size will change across releases; treat ~10 KB as a design budget, and re-check Network tab after upgrades.
If that tradeoff matches how your team works, start with the install guide and the Widget API—not with a React wrapper we deliberately never shipped.
Next step
Continue with the most relevant CanWeChat guide for this topic.