Showing a What's New Page After an Update
Announce new features after an MV3 extension update without annoying users: detecting meaningful version changes in onInstalled, badge hints instead of tabs, an in-extension changelog, and opt-out.
Table of Contents
You shipped a feature users have been asking for, and nobody notices it. The obvious fix — open a “What’s new” tab on every update — makes the problem worse: extensions auto-update in the background, often several times a month, and a tab appearing out of nowhere in the middle of someone’s work reads as spam. Store reviewers and users both dislike it. A good release announcement is quiet by default and discoverable on demand: a subtle hint on the toolbar icon, a short “new” section in the popup, and a full changelog one click away, shown only for releases that actually contain something worth announcing. This guide builds that. It belongs to extension updates and data migration.
When an update is worth announcing
chrome.runtime.onInstalled fires after every update with reason: "update" and the previousVersion. Most updates are fixes and internal changes that users should never hear about. A minority add something the user would want to know: a new feature, a changed behaviour, a removed option. The decision belongs in data you ship with the release, not in code: a small changelog file listing each version’s user-facing notes and whether they merit a notice. On update, the worker compares the previous and current versions, collects any notable entries in between, and decides how loud to be — usually a badge, occasionally a popup section, rarely a tab.
Step-by-step: quiet, discoverable release notes
1. Ship a structured changelog with the release
1// changelog.json (packaged)
2[
3 { "version": "2.6.0", "level": "feature", "title": "Highlights sync across devices",
4 "body": "Your highlights now follow you to every browser where you're signed in.", "link": "options.html#sync" },
5 { "version": "2.5.3", "level": "fix", "title": "Fixed export of very long notes" },
6 { "version": "2.5.0", "level": "change", "title": "Reader mode now uses your system font",
7 "body": "You can switch back under Appearance.", "link": "options.html#appearance" }
8]
Execution context: a static file in the package, read with fetch(chrome.runtime.getURL("changelog.json")). Keeping release notes as data means the logic never changes per release — only the file does — and the same file feeds the in-extension changelog page. level drives how the update is surfaced.
2. Decide what to surface in onInstalled
1// sw.js
2chrome.runtime.onInstalled.addListener(async ({ reason, previousVersion }) => {
3 if (reason !== "update" || !previousVersion) return;
4 const current = chrome.runtime.getManifest().version;
5 if (compareVersions(previousVersion, current) >= 0) return; // reload or rollback
6 const log = await (await fetch(chrome.runtime.getURL("changelog.json"))).json();
7 const fresh = log.filter((e) => compareVersions(e.version, previousVersion) > 0 && compareVersions(e.version, current) <= 0);
8 const notable = fresh.filter((e) => e.level !== "fix");
9 if (notable.length === 0) return;
10 await chrome.storage.local.set({ unseenRelease: { from: previousVersion, to: current, items: notable } });
11 await chrome.action.setBadgeText({ text: "NEW" });
12 await chrome.action.setBadgeBackgroundColor({ color: "#0d9488" });
13});
14
15function compareVersions(a, b) {
16 const pa = a.split(".").map(Number), pb = b.split(".").map(Number);
17 for (let i = 0; i < Math.max(pa.length, pb.length); i++) if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
18 return 0;
19}
Execution context: the service worker. Collecting every notable entry between the previous and current version covers users who skipped releases. A reload during development (same version) or a rollback (lower version) produces nothing. The badge is a hint, not an interruption; it stays until the user opens the popup.
3. Show a compact section in the popup
1// popup.js
2const { unseenRelease } = await chrome.storage.local.get("unseenRelease");
3if (unseenRelease) {
4 renderWhatsNew(unseenRelease.items.slice(0, 3)); // two or three lines, each with a link
5 await chrome.storage.local.remove("unseenRelease");
6 await chrome.action.setBadgeText({ text: "" });
7}
Execution context: the popup. A short section at the top — title, one sentence, a link to the relevant setting — is enough. Marking it seen on first display keeps it from repeating; a “See all changes” link opens the full changelog page for users who want more.
4. Provide a full changelog page
1// changelog.html → changelog.js
2const log = await (await fetch(chrome.runtime.getURL("changelog.json"))).json();
3for (const e of log) {
4 const h = document.createElement("h2"); h.textContent = `${e.version} — ${e.title}`;
5 const p = document.createElement("p"); p.textContent = e.body ?? "";
6 main.append(h, p);
7}
Execution context: an extension page opened from the popup or options. The packaged changelog works offline and always matches the installed version. Render with textContent even though the file is yours — habits matter.
5. Reserve tabs for genuinely breaking changes
1if (notable.some((e) => e.level === "breaking")) {
2 const { lastWhatsNewTab = 0 } = await chrome.storage.local.get("lastWhatsNewTab");
3 if (Date.now() - lastWhatsNewTab > 90 * 86_400_000) { // at most once a quarter
4 await chrome.tabs.create({ url: chrome.runtime.getURL("changelog.html#breaking"), active: false });
5 await chrome.storage.local.set({ lastWhatsNewTab: Date.now() });
6 }
7}
Execution context: the service worker. When a change could cause data loss or confusion — a removed feature, a changed default that affects privacy — the user should learn about it even if they rarely open the popup. Opening the tab in the background (active: false) avoids stealing focus. A frequency cap prevents the extension from becoming the one that opens tabs every week.
6. Let users opt out
1// options.js
2quietToggle.addEventListener("change", () => chrome.storage.sync.set({ quietUpdates: quietToggle.checked }));
3// sw.js — honour it before setting a badge or opening anything
4const { quietUpdates } = await chrome.storage.sync.get("quietUpdates");
5if (quietUpdates) return;
Execution context: the options page and worker. Some users do not want announcements at all. A single setting, synced across devices, respects that and costs nothing.
7. Never announce on first install
The first-run experience is different: reason: "install" is the moment for onboarding, covered in showing a first-run setup page after install. A new user does not need to know what changed from a version they never had.
Common mistakes
- Opening a tab on every update. Users and reviewers treat it as spam.
- Announcing fix-only releases. It trains users to ignore announcements.
- Ignoring skipped versions. Users jumping several versions miss notes in between.
- Announcing after a rollback. A lower version is not news.
- Hard-coding notes in the worker. Keep them in a data file.
Cross-browser variation
- Chrome / Edge:
onInstalledwithpreviousVersion; badge and popup approach works as described. - Firefox: same events; AMO also shows release notes on the add-on listing — keep them consistent with your changelog.
- Safari: updates arrive with the containing app;
onInstalledwithreason: "update"still fires for the web extension, and the App Store shows “What’s New” text separately.
Verification
- Load version 2.5.3 unpacked, then bump to 2.6.0 and reload: badge shows NEW, popup shows the 2.6.0 entry once.
- Bump from 2.6.0 to 2.6.1 (fix-only): nothing appears.
- Jump from 2.4.0 to 2.6.0: entries for 2.5.0 and 2.6.0 both appear.
- Enable quiet updates and confirm no badge appears on the next notable update.
FAQ
Should release notes be localised?
Yes, if the rest of the extension is. Use one changelog file per locale or _locales messages keyed by version.
Can I show the changelog from the store listing instead?
Link to it, but in-extension notes reach users without leaving the browser and always match the installed version.
What about enterprise users?
Managed installs often prefer silence; check chrome.management.getSelf() for installType: "admin" and skip badges.
How do I measure whether users notice new features?
Count opens of the What’s new section and clicks on its links, aggregated and with consent, and compare feature usage before and after the release. If almost nobody clicks, the announcement copy or placement needs work — not more interruption.
Related
- Running data migrations on onInstalled — the other job of the update event.
- Versioning and changelogs for extension releases — producing the changelog in CI.
- Badge text, colour and count patterns — badge design.
- Extension updates and data migration — the parent topic.