Web-Dev-For-Beginners 浏览器扩展第三课:后台任务、动态图标与 Web 性能剖析实战
本文围绕 Web-Dev-For-Beginners 仓库中“构建浏览器扩展”系列的最后一课——后台任务与性能(Background Tasks and Performance)展开。你将先学会用浏览器开发者工具的 Performance 面板定位 Web 性能瓶颈,再基于 chrome.runtime 消息传递 API 与 Canvas 绘图,为碳足迹追踪扩展实现“随电网碳排放强度变色的工具栏图标”,最终获得一个完整可用、并理解性能原理的浏览器扩展。
1. 课程定位:为碳足迹扩展补上“后台任务”
在系列的前两课中,你已经完成了以下工作(见 扩展模块总览):
本扩展是一个“迷你网站式”的特定任务工具:用户输入 API Key 与地区代码后,扩展查询该地区的电网碳排放强度,用来看此刻是否适合运行烘干机等高碳活动。目前它缺少最后一块拼图——让工具栏图标根据实时碳排放数据变色,同时顺带掌握浏览器如何处理这类后台任务。这正是本课要解决的:
- 性能基础:用开发者工具的 Performance 面板采集与解读性能数据;
- 后台任务:通过
chrome.runtime消息机制更新扩展图标; - 收尾验证:重新构建并加载扩展,观察图标颜色随数据变化。
2. Web 性能基础:先测量,再优化
“网站性能关乎两件事:页面加载有多快,以及页面上的代码运行有多快。” —— Zack Grossbart
“如何让网站在所有设备、所有用户、所有场景下都快”是一个庞大的话题。文档给出的第一个原则是:确保站点高效运行的第一步,是收集它的性能数据,而第一站就是浏览器内置的开发者工具。
2.1 打开 Performance 面板并录制
在 Edge 中,点击右上角“设置及其他”(三点图标)→ “更多工具” → “开发者工具”,打开 Performance 选项卡;也可使用快捷键:
- Windows:
Ctrl+Shift+I - Mac:
Option+Command+I
操作步骤(文档给出的标准流程):
- 打开目标网页(文档建议例如微软官网);
- 点击 Record(录制) 按钮;
- 刷新该页面,让 Profiler 捕获脚本执行、渲染、绘制的全过程;
- 随时停止录制,即可看到针对该网页 script / render / paint 三条主线的详细时间线。
录制产物示例(时间线视图,可见 Scripts、Rendering、Painting 等轨道):
提示:想获得真实的“首次启动”耗时读数,测试前先清空浏览器缓存——首访与回访的加载表现往往差异很大。
2.2 解读摘要面板与事件日志
- 缩放时间线:选中 profile 时间线上的片段,可放大查看页面加载期间发生的事件;
- 查看摘要:选中时间线的一部分后,在 summary 面板中获得页面性能的“快照”:
- 检查 Event Log:在事件日志面板中查找耗时超过 15 ms 的事件,这是定位卡顿的常用阈值:
文档还布置了一个自我练习:对当前正在学习的这个网站本身打开开发者工具,找出加载最慢与最快的资产(asset)分别是什么。
3. Profiling 时该盯住什么:三大经典“问题源”
跑完 Profiler 只是开始,真正的技能是读懂这些图表。文档总结了三类几乎每个 Web 项目都需警惕的性能隐患:
3.1 资产体积(Asset sizes)
Web 页面近年来整体“变重”,而增重相当一部分来自图片。良好实践是:
- 确保图片经过压缩优化;
- 以匹配用户设备的正确尺寸与分辨率下发图片(不必向手机推送桌面级大图);
- 进一步可配合 CSS/JS 压缩与懒加载(lazy loading),让图片在用户滚动到可视区时再下载。
3.2 DOM 遍历成本(DOM traversals)
浏览器必须基于你写的代码构建 Document Object Model,因此为了页面性能应:
- 保持 HTML 标签数量与嵌套层级最小化,只用、只样式化页面真正需要的元素;
- 优化关联到页面的“过剩 CSS”——例如只在某一页使用的样式,不必塞进主样式表。
3.3 渲染阻塞的 JavaScript(render-blocking)
每位 JS 开发者都应警惕必须先于 DOM 遍历与绘制完成的阻塞脚本。文档建议:对行内/外链脚本考虑使用 defer 属性(系列中的 Terrarium 模块即这样处理),让脚本在 DOM 解析完成后再执行,避免阻塞渲染。
完成这三项检查后,就可以进入本课的实操:用后台任务给扩展加上动态图标。
4. 编写颜色计算函数 calculateColor()
4.1 算法思路
把数值型碳强度数据翻译成有意义的颜色——文档类比“交通灯”:绿代表清洁能源,深棕代表高碳。色标定义如下(单位:克 CO2 / 千瓦时):
| 色标区间 | 颜色 | 语义 |
|---|---|---|
| 0 – 150 | #2AA364(绿) |
清洁 |
| 150 – 600 | #F5EB4D(黄) |
中等 |
| 600 – 750 | #9E4229(橙) |
偏高 |
| 750 – 800 | #381D02(深棕) |
很高 |
| ≥ 800 | #381D02(深棕) |
很高 |
4.2 完整代码
在 index.js 中,为你之前用于获取 DOM 元素的一组 const 变量之后,添加名为 calculateColor() 的函数(课程讲义版本):
function calculateColor(value) {
let co2Scale = [0, 150, 600, 750, 800];
let colors = ['#2AA364', '#F5EB4D', '#9E4229', '#381D02', '#381D02'];
let closestNum = co2Scale.sort((a, b) => {
return Math.abs(a - value) - Math.abs(b - value);
})[0];
console.log(value + ' is closest to ' + closestNum);
let num = (element) => element > closestNum;
let scaleIndex = co2Scale.findIndex(num);
let closestColor = colors[scaleIndex];
console.log(scaleIndex, closestColor);
chrome.runtime.sendMessage({ action: 'updateIcon', value: { color: closestColor } });
}
这段代码发生了什么:
- 输入上一课 API 调用拿到的碳强度值(carbon intensity);
- 用
sort+ 距离差的比较器找出co2Scale中离value最近的刻度,存入closestNum; - 用
findIndex找到第一个大于closestNum的刻度索引scaleIndex,即value所落入区间对应的颜色下标; - 通过
chrome.runtime.sendMessage把选中的颜色发给后台脚本。
一个值得注意的边界细节(从源码结构看):findIndex 在找不到“大于 closestNum 的元素”时会返回 -1,此时 colors[-1] 为 undefined。对照仓库内给出的参考答案 solution/src/index.js,教学版保留了这一简化逻辑,且把该函数声明为 async 并写成赋值给全局变量(calculateColor = async (value) => {...}),说明这是面向初学者的教学实现而非生产级代码;生产实现应对越界值做兜底。
4.3 chrome.runtime API 与 Edge 兼容性
chrome.runtime 是扩展的“神经系统”,官方描述(课程文档引自 Chrome 文档):
“Use the chrome.runtime API to retrieve the background page, return details about the manifest, and listen for and respond to events in the app or extension lifecycle. You can also use this API to convert the relative path of URLs to fully-qualified URLs.”
它承担的职责包括:获取后台页、返回 manifest 详情、监听并响应扩展生命周期事件、在不同脚本上下文之间传递消息(本扩展就依赖 sendMessage / onMessage 这一对接口)。
✅ 一个容易困惑的点:如果你是在 Edge 上开发这个扩展,会发现用的竟是 chrome.* API。原因是新版 Edge 运行在 Chromium 引擎上,这些 Chrome 扩展工具可以直接复用。
提示:若要给浏览器扩展做性能分析,请从扩展自身内部打开开发者工具——扩展是它自己独立的浏览器上下文实例,只有这样才能看到扩展专属的性能指标。
5. 设置默认图标颜色
在数据到达之前,给扩展一个“起始状态”,避免用户看到空白或看起来损坏的图标。在 init() 函数中,通过再次调用 updateIcon 动作把图标设为通用绿色:
chrome.runtime.sendMessage({
action: 'updateIcon',
value: {
color: 'green',
},
});
这一步同时完成了三件事:给扩展一个中性的默认状态;在扩展加载瞬间即提供视觉反馈;并提前“打通”与后台脚本的通信模式,确保数据加载完成前用户看到的也是功能正常的扩展。
对照仓库参考答案,这段消息发送位于 solution/src/index.js 的 init() 函数开头(先发送绿色图标消息,再读取 localStorage 中的 apiKey / region 决定展示表单还是直接拉取数据),与课程讲义描述一致。
6. 串联调用链:API 回调 → 计算颜色 → 后台监听
6.1 在 API 回调中调用 calculateColor
接下来调用刚写的函数——把它挂到 CO2 Signal API 返回的 Promise 上:
//let CO2...
calculateColor(CO2);
仓库参考答案中的完整链路可见 solution/src/index.js:displayCarbonUsage() 使用 axios 请求 https://api.co2signal.com/v1/latest(携带 countryCode 查询参数与 auth-token 请求头),在 .then 回调中先校验 carbonIntensity / fossilFuelPercentage 字段,再取 Math.floor(data.carbonIntensity) 并执行 calculateColor(CO2),最后才更新界面。也就是说,图标更新被嵌入异步数据流之中:新数据一到,颜色随之自动变化,且数据获取与展示逻辑保持职责分离。
6.2 后台脚本:消息监听 + Canvas 动态绘制图标
最后,在 /dist/background.js 中为这些后台动作添加监听器(课程讲义版本):
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
if (msg.action === 'updateIcon') {
chrome.browserAction.setIcon({ imageData: drawIcon(msg.value) });
}
});
// 借鉴自 lollipop 扩展的一个巧妙做法
function drawIcon(value) {
let canvas = document.createElement('canvas');
let context = canvas.getContext('2d');
context.beginPath();
context.fillStyle = value.color;
context.arc(100, 100, 50, 0, 2 * Math.PI);
context.fill();
return context.getImageData(50, 50, 100, 100);
}
工作原理:
chrome.runtime.onMessage是所有发往后台任务处理器的消息的统一入口(类似前台接线员);- 消息的
action字段为'updateIcon'时,进入后续分支; drawIcon()使用 Canvas API 现场绘制一枚彩色圆形:beginPath()→ 设置fillStyle为消息携带的颜色 →arc(100, 100, 50, ...)画半径 50 的圆 →fill()填充,最后用getImageData(50, 50, 100, 100)截取 100×100 像素的ImageData作为图标位图;setIcon({ imageData })立即把新位图应用到工具栏图标。
关于“从源码结构看”的一处演进:仓库英文讲义中的新版示例把绘制改为了 new OffscreenCanvas(200, 200) 并改用 chrome.action.setIcon(对应 Manifest V3 中取代 browserAction 的 action API),说明图标动态更新这条链路在新版扩展架构里同样成立,只是 API 命名与画布创建方式随浏览器版本迭代而更新。
完整的消息流如下:
内容脚本 calculateColor(CO2)
│ chrome.runtime.sendMessage({action:'updateIcon', value:{color}})
▼
background.js onMessage 监听器
│ drawIcon(value):Canvas 画圆 → getImageData(100×100)
▼
browserAction/action.setIcon({imageData})
▼
工具栏图标实时变色
关于 Canvas API 的更多细节,可在 Space Game 之绘制到画布一课 中继续学习。
7. 构建、加载与验证
完成上述代码后执行验证流程(构建环境要求见 solution/package.json:Node ≥ 18、npm ≥ 9,构建脚本为 webpack):
npm run build
- 运行
npm run build重新打包扩展; - 在扩展管理页**重新加载(reload)**该扩展——这是最容易漏掉的一步;
- 打开扩展,输入 API Key 与地区,提交表单;
- 观察工具栏图标:从默认的通用绿色,变为与实时碳强度匹配的黄/橙/深棕色。
现在你已拥有一个可用的浏览器扩展,并顺带掌握了浏览器的工作原理与性能剖析方法——看一眼图标,就知道现在是不是运行烘干机的好时机。
8. 挑战题与课后练习
挑战题:调研几个存在多年的开源网站,基于它们的提交历史,判断其多年来为性能做了(或没做)哪些优化?最常见的“痛点”在哪里?实操建议:在提交信息中搜索 “optimize”“performance”“faster” 等关键词,观察是否反复出现同类问题的修复。
课后自查:
- 观察不同浏览器“性能”标签页,看看它们测量 Web 性能的方式有何显著差异;
- 考虑订阅一份性能类技术通讯,持续跟进行业动态;
- 正式作业见 分析网站性能:提交一份详细报告,指出目标网站中性能存在问题的区域,分析其缓慢原因并给出加速方案;评分标准要求报告不能只依赖浏览器工具,还需引入第三方工具的数据佐证(评分细则见 assignment.ms.md)。
9. 小结:本扩展架构中“后台任务”的三个要点
- 测量先行:任何性能优化都始于开发者工具 Performance 面板的录制与解读——15 ms 事件阈值、资产体积、DOM 复杂度、渲染阻塞脚本是四类高频瓶颈;
- 消息传递解耦:
chrome.runtime.sendMessage/onMessage让内容脚本与后台脚本职责分离,UI 逻辑不直接接触图标系统; - Canvas 动态图标:
arc+fill+getImageData几行代码即可现场生成位图,配合setIcon完成实时视觉反馈;在 Edge 等 Chromium 内核浏览器上可复用同一套chrome.*API。
对照仓库参考答案 solution/src/index.js 通读一遍完整实现(DOM 引用 → 表单提交 → localStorage 持久化 → API 调用 → 颜色计算 → 图标消息),即可把本课的每个片段拼回整体。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00



