百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

240个标签页打乒乓?开发者用Chrome浏览器“整活”,网友:再玩内存要炸了!

myzbx 2025-03-02 18:11 52 浏览

编译 | 苏宓
出品 | CSDN(ID:CSDNnews)

在浏览器网页里面玩游戏不稀奇,但用没关闭的浏览器标签页来玩,那可就太少见了!

最近,有位极具想象力的开发者 Nolen Royalty 看着自己一堆没关的标签页,觉得它们不仅“扎眼”,还占了太多屏幕空间。于是,秉持着程序员“能动手就绝不忍着”的精神,他在这堆没有关闭的标签页上复现了一个乒乓球游戏《Pong》,而且是 240 个标签页一起玩!

不仅如此,Nolen Royalty 还直接在 GitHub 上开源了他的相关实现代码,让这些标签页真正“有用”起来:https://github.com/nolenroyalty/faviconic。需要注意的是,因为其在开发过程中使用了一款 AppleScript 工具,这是苹果系统独有的,所以这个游戏只能在 macOS 系统下的 Chrome 浏览器上抢先体验~

正如下面视频所示,Nolen 把 240 个浏览器标签页排成了一个 8x30 的紧密网格,然后让它们一起运行《Pong》!游戏的球和挡板可以顺畅地在前端窗口的画布和所有标签页之间移动。

那么他是如何做到的,我们将通过其发布的博文一探究竟。


灵感来源

Nolen 称,这个项目的灵感来自他的朋友 Tru。同样身为开发者的 Tru 于上周用浏览器的小图标(favicon)做了一个能运行《Flappy Bird》的版本,叫做 Flappy Favi(https://mewtru.com/flappyfavi)。

注:favicon 是网站的小图标,通常出现在浏览器标签页的左上角。它的作用是帮助用户快速识别网站,通常是一个 16x16 或 32x32 像素的小图像,比如网站的 logo 或代表该网站的符号。每个网站都有自己独特的 favicon,用来提升用户体验,让浏览网页时更具辨识度。

把 FlappyFavi 放在标签页上玩,也是众人没想到的,不过,虽然这个很有创意,但是由于 favicon 实在太小了,游戏画面几乎看不清,玩起来简直是在挑战眼力的极限。

于是,Nolen 想能不能有个更好方法解决这个问题?

当然最为直接的方式就是让界面变大一些!Nolen 所能想到的最佳方案,就是把画面拆分到多个标签页上显示。但这带来了几个难题:

1.怎么创建一个整齐的标签页网格来展示画面?

2.这些标签页在后台时,如何保持更新?

3.这些标签页如何协调同步?


试验过程

针对上述的第一个问题,即如何创建标签页网格。Nolen 最开始的方法很简单,首先打开 Chrome 浏览器,疯狂点“新建标签页”按钮,直到标签页变得足够小。最终,它看起来像这样:

一排小小的标签页,favicon 形成了一个网格

其实 Nolen 只需要再打开一个 Chrome 窗口,调整好位置,就能再增加一行标签页。

不过,Nolen 的最终目标是创建一个超大的标签页网格,当前这种手动操作起来太麻烦了。所以,他决定借助一款工具——AppleScript 来帮助实现。AppleScript 是 macOS 上的一款强大的工具,能用接近英语的语法控制程序。虽然它的语法有点冗长,写起来像是“加了很多废话的 Python”,但在这种情况下,它非常管用。

Nolen 写了一个脚本,让它自动打开 8 个 Chrome 窗口,每个窗口里有 30 个标签页,而且还会把这些窗口精确地叠放在一起,形成一个整齐的标签页网格,就像这样:

在尝试过程中,Nolen 也遇到了一些烦人的小问题,比如 Chrome 会尝试恢复上次关闭的标签页,所以脚本需要在一开始时就得清除这些标签页。

不过,最终代码其实挺简单的。核心部分是这样的:

-- Set the window bounds (x, y, width, height)set bounds of newWindow to {x, y, x + width, y + height}global tabCountset tabCount to 0
tell newWindow set URL of active tab to baseURL & "windowIndex=" & windowNum & "&tabIndex=" & tabCountend tell
set tabCount to (tabCount + 1)
-- Create the specified number of tabsrepeat (numTabs - 1) times tell newWindow if windowNum is (maxWindows - 1) and tabCount is (numTabs - 1) then make new tab with properties {URL:baseUrl & "windowIndex=" & windowNum & "&tabIndex=" & tabCount & "&isMain=true&numWindows=" & maxWindows & "&numTabs=" & numTabs & "&fullWidth=" & fullWidth} else make new tab with properties {URL:baseURL & "windowIndex=" & windowNum & "&tabIndex=" & tabCount} end if end tell set tabCount to (tabCount + 1)end repeat

如何快速更新 favicon(网站小图标)

接下来的问题是:怎么更新 favicon?

通常,浏览器会从某些固定的 URL 读取 favicon,但我们也可以在 HTML 代码的 里指定一个 favicon 位置。只要更新这个元素,浏览器就会刷新图标。FlappyFavi 就是靠这个原理实现的。

“根据我的观察,Chrome 大约每秒能刷新 favicon 4 次”,Nolen 说道。

但他似乎也不确定这个方法在后台标签页里还能不能用。为了节省资源,浏览器会对后台标签页做限制,而 Nolen 的大部分标签页都是在后台!

为此,他用了一个简单的测试方法——每 250 毫秒更新一次 favicon,看看后台标签页的表现。结果发现:后台标签页的更新频率被降到了大约 1 次/秒!

后台标签页太慢了!

Nolen 发现,他的 setInterval 定时器被浏览器限制了!于是,他开始思考有没有办法绕过这个限制。

他的第一个想法是利用 Web Audio API(https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)。因为音频在后台依然可以流畅播放,而且可以绑定一些回调函数。Nolen 尝试播放一个人耳听不到的音频,然后把更新代码放进音频线程里,希望能骗过浏览器。但失败了。

然后他试了 Web Worker(https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers)。Web Worker 允许开发者把计算任务放到浏览器的后台线程里运行,这样就不会影响页面渲染。起初,Nolen 猜测 Web Worker 可能不会被限制得那么严重。

于是,他把定时器代码移到了 Web Worker 里,让它定时向主页面发送消息,触发 favicon 更新。结果——成功了!

所有标签页的 favicon 都能同步更新了!

这部分的代码有些长,但逻辑很简单:Web Worker 生成一组 emoji,把它们转换成数据 URL,并传回主页面更新 favicon。

Web Worker 图标代码:

// worker
let intervalId = ;let counter = 0;const emojis = ["??", "??", "?", "??", "??"];let currentIndex = 0;
function drawEmoji(emoji) { // Create an OffscreenCanvas (supported in workers) const canvas = new OffscreenCanvas(32, 32); const ctx = canvas.getContext("2d");
ctx.font = "28px serif"; ctx.fillText(emoji, 2, 24);
// Convert to blob and send back canvas.convertToBlob().then((blob) => { const reader = new FileReader(); reader.onloadend = () => { counter++; postMessage({ type: "update", dataUrl: reader.result, counter: counter, }); }; reader.readAsDataURL(blob); });}
self.onmessage = function (e) { if (e.data.command === "start") { const interval = e.data.interval; if (intervalId) clearInterval(intervalId);
intervalId = setInterval(() => { drawEmoji(emojis[currentIndex]); currentIndex = (currentIndex + 1) % emojis.length; }, interval);
// Initial update drawEmoji(emojis[currentIndex]); } else if (e.data.command === "stop") { if (intervalId) { clearInterval(intervalId); intervalId = ; } }};
// main documentworker.onmessage = function (e) { if (e.data.type === "update") { let link = document.querySelector("link[rel*='icon']") || document.createElement("link"); link.type = "image/x-icon"; link.rel = "shortcut icon"; link.href = e.data.dataUrl; document.head.appendChild(link); }};

如何让所有标签页同步?

但问题来了:如果要让所有标签页配合一起运行,我们需要它们互相通信。

这涉及两个问题:

  • 每个标签页怎么知道自己的位置?(比如:我是第二个窗口的第三个标签页。)

  • 标签页之间用什么方式通信?

第一个问题比较简单。此前,Nolen 在 AppleScript 代码里已经做了处理——脚本会把窗口编号和标签页编号作为 URL 参数传进去。这样,每个标签页只需要解析自己的 URL,就能知道自己的 X 和 Y 坐标。

针对第二个问题,Nolen 最初的想法是使用 WebSocket,让所有标签页连接到一个服务器,由服务器统一指挥它们该做什么。

为此,他做了个简单的概念验证。加载时,每个标签页在 Web Worker 里创建 WebSocket 连接,然后服务器给每个标签页发送数据,告诉它更新 favicon。服务器会区分奇数和偶数编号的标签页,让它们显示不同的图像。

这基本可行,但有两个问题:

1. 就 Nolen 个人而言,他不想用服务器!他更希望这个方案可以在任何浏览器里运行,而不需要额外搭建服务器。

2.同步问题:由于每个标签页的加载速度不同,导致它们的更新不同步。

为了处理第一个问题,Nolen 选择使用 BroadcastChannel 让所有标签页同步——这是一种在同一域名下在不同标签页之间分发信息的方式。与 WebSockets 不同,后者是点对点通信,而 BroadcastChannel 是“一对多”的方式。对 Nolen 来说,这似乎更符合他的需求。

至于第二个问题,Nolen 让后台标签页通过 BroadcastChannel 发送注册消息,消息中包含它们的标签和窗口索引。主标签页(即前台标签页,未被限制)则监听这些消息,并在收到消息后发送确认,之后后台标签页就不再尝试注册。等到主标签页接收到所有后台标签页的注册事件后,动画才开始运行。

这样,所有标签页都能同时更新 favicon,不会有不同步的问题了!

// workerbc = new BroadcastChannel("bc");bc.addEventListener("message", (event) => { const msg = event.data; if (!msg) return; else if ( msg.type === "ack" && msg.tabIndex === tabIndex && msg.windowIndex === windowIndex ) { clearInterval(regInterval); registrationDone = true; postMessage({ type: "registration-ack" }); }});
regInterval = setInterval(() => { bc.postMessage({ type: "register", tabIndex, windowIndex });}, 1000);
// main tabconst bc = new BroadcastChannel("bc");const registrations = {};if (data && data.type === "register") { const key = `tab_${data.tabIndex}_${data.windowIndex}`; console.log(`Registered: ${key}`); registrations[key] = true; bc.postMessage({ type: "ack", tabIndex: data.tabIndex, windowIndex: data.windowIndex, });
const expected = numTabs * numWindows; if (Object.keys(registrations).length === expected) { console.log("All tabs registered. Beginning..."); runLoopGeneric({ bc, worker, numTabs, numWindows, fullWidth, impl: "pong", }); }}

从 Canvas 到标签页

当 Nolen 成功控制所有标签页后,他开始思考:到底应该在这些标签页上显示什么内容?

他觉得,如果能在主标签页(前台窗口)里绘制一个图形,并让它从主窗口“移动”到标签栏上,那将会非常酷!

于是,他决定从一个简单的矩形开始做实验……

为了实现这个目标,他需要想象有一个“画布”(canvas),它从前台窗口延伸到上面的所有标签页图标(favicon),然后根据物体的位置,在这些图标和主画布上同时进行绘制。

在做这个尝试时,Nolen 花了很多时间进行测量。

例如,他发现从 Chrome 窗口的左侧到第一个 favicon 之间的距离是 92px(至少在这种标签页数量下是这样)。从 favicon 底部到窗口顶部的距离是 58 px。favicon 的大小是 16x16 像素,类似这样的测量数据,还有很多……

他的代码利用这些测量值,以及当前打开的窗口和标签页数量,以此来:

  • 计算画布的宽度,使其能够与上方的 favicons 完美对齐。

  • 计算每个标签页的宽度。

  • 确定整个“画布”的宽度和高度,包括 favicon 区域、地址栏以及窗口内部的实际画布。

然后,他模拟一个矩形在整个“画布”上移动。当矩形位于 URL 栏以下时,他在“真实画布”上绘制它。同时,他计算出矩形在 URL 栏以上的部分,并将这些信息广播给其他标签页。每个标签页根据之前计算的像素坐标,更新自己的 favicon,改变对应的像素为黑色或白色,以同步动画效果。

矩形的代码:

// main tabfunction transmitSquareCoords() { const copied = { ...square }; updateMap = {}; for (let t = 0; t < numTabs - 1; t++) { for (let w = 0; w < numWindows; w++) { pixels = []; const PIXEL_COUNT = 4; const FAVICON_SIZE = 16; const MULT = FAVICON_SIZE / PIXEL_COUNT; for (let yy = 0; yy < PIXEL_COUNT; yy++) { for (let xx = 0; xx < PIXEL_COUNT; xx++) { let x = tabSingle * t + (tabSingle - 16) / 2 + xx * MULT; let y = TOP_TO_FAVICON + HARDCODED_WINDOW_DIFF * w + yy * MULT; let thisSquare = { x, y, w: MULT, h: MULT, }; if (intersects(thisSquare, copied)) { pixels.push(1); } else { pixels.push(0); } } } const key = `tab_${t}_${w}`; updateMap[key] = pixels; } } bc.postMessage({ type: "update", pixels: updateMap });}
// background tabfunction updateFavicon(pixels) { const canvas = document.createElement("canvas"); canvas.width = 4; canvas.height = 4; const ctx = canvas.getContext("2d"); for (let i = 0; i < 16; i++) { const x = i % 4, y = Math.floor(i / 4); ctx.fillStyle = pixels[i] ? "#000" : "#fff"; ctx.fillRect(x, y, 1, 1); } const faviconURL = canvas.toDataURL("image/png"); let link = document.querySelector("link[rel='icon']"); if (!link) { link = document.createElement("link"); link.rel = "icon"; document.head.appendChild(link); } link.href = faviconURL;}

提升性能

这个方案基本能跑起来,但它的资源占用超乎预期——动画在画布上时不时会卡顿,它是用 requestAnimationFrame 来绘制的,按理说应该是很平滑的。

卡顿意味着前台标签页的线程占用了太多资源。Nolen 检查了一下,发现前台页面的逻辑并不复杂,所以他猜测可能是数据传输的问题。

最开始,他的主线程会计算每个 favicon 像素的状态,然后把完整的数据广播给所有标签页。这样可能会导致大量的复制操作——每个标签页都得单独拷贝数据,这可能会带来很大的负担。虽然他对此有所怀疑,但也没有更好的猜测。

于是,他修改了代码,直接广播方块的位置,让每个标签页自己计算是否需要更新自己的 favicon。但是效果并没有改善,问题依然存在。

最后,他决定采用经典的调试方法,逐步禁用代码,看看是哪一部分导致了卡顿。最终,他发现问题出在了生成 favicon 上:他的代码每秒钟生成上百个 favicon,导致性能大幅下降。

基本上,Nolen 编写的代码会在每个标签页里做了类似这样的事情,用来创建一个 4x4 的黑白图像,并将其转化为一个 URL,以便指向 favicon。

const ctx = bwCanvas.getContext("2d");for (let i = 0; i < len; i++) { const x = i % width; const y = Math.floor(i / width); const index = (y * width + x) * 4; ctx.fillStyle = pixels[i] ? BLACK : WHITE; ctx.fillRect(x, y, 1, 1);}return bwCanvas.toDataURL("image/png");

这段代码会在每一帧重新执行,无论画布是否发生变化。结果就是,每秒钟有上百个标签页都在重新生成几乎相同的白色小方块 favicon,导致 CPU 占用率过高。

于是,他修改了代码,只有当 favicon 实际发生变化时才更新,性能也立刻得到了大幅提升。

我更新了代码,只在 favicon 发生变化时才更新,这样性能得到了显著提升。

做点有趣的东西

优化完动画后,Nolen 把代码整理了一下,相当于做了一个小型的“引擎”,然后开始琢磨着可以用它做点什么有趣的东西。

最开始,Nolen 想到了贪吃蛇(Snake)。因为贪吃蛇的格子化运动跟 favicon 的像素特点很搭,而且游戏逻辑也不复杂,于是他很快就写了一个简单的版本。

但很快,Nolen 遇到一个问题——你可能也能猜到。

贪吃蛇的格子式移动方式太“离散”了。之前那个矩形动画从“连续”的画布平滑移动到“离散”的 favicon 逐步点亮,看起来很自然。而贪吃蛇本身就是基于固定格子的,格子里移动的感觉让整个游戏看起来有点不对劲。

于是,Nolen 再想了想,最后决定做个《Pong》游戏(乒乓球)。因为《Pong》里的球和挡板需要在画布和标签页之间来回移动,这种视觉效果应该挺酷的。

实现 Pong 游戏

然后,Nolen 就做了个 Pong 游戏!

这个过程其实没什么复杂的,因为他已经很熟悉做游戏的流程,而且《Pong》本身也挺简单,特别是当他已经有了一套成熟的绘制 API。

不过,Nolen 在博客中还是提到了一些实现的细节:

  • 电脑玩家(右边的挡板)会努力让自己的挡板中心对齐球的中心。

  • 他用了一些简单的三角函数来计算球反弹的角度,让球的反弹不像真实物理那样死板,而是有点变化,这样游戏就不会变得太无聊。

  • 他又写了一次“判断两个方块是否相交”的函数——估计这是他第 20 次写这种逻辑了。

  • 他还对球的移动做了平滑处理,并加了个拖尾效果,试图让球看起来更流畅、更有动感。

Nolen 还开玩笑说:“说实话,我盯着这个拖尾效果看了太久,现在搞不清楚它到底是酷还是丑了……”

时下这部分代码已经开源了,但 Nolen 也坦言:“说实话,代码质量一般,因为一直没从‘原型模式’中脱离,没做什么精细优化。如果你感兴趣,可以去 GitHub 看看(https://github.com/nolenroyalty/faviconic),但别嫌弃代码写得乱。”

在 Nolen 的创意发布后,大家都纷纷表示:“一如既往的令人印象深刻!”不仅是因为他成功把标签页变成了一个可以玩游戏的地方,更因为这个项目展现了他新奇的创意。当然,还有不少人开玩笑说:“内存:游戏很好,下次别再玩了!”

来源:

https://eieio.games/blog/running-pong-in-240-browser-tabs/#toc:from-canvas-to-tab-bar

https://news.ycombinator.com/item?id=43119086


相关推荐

如何设计一个优秀的电子商务产品详情页

加入人人都是产品经理【起点学院】产品经理实战训练营,BAT产品总监手把手带你学产品电子商务网站的产品详情页面无疑是设计师和开发人员关注的最重要的网页之一。产品详情页面是客户作出“加入购物车”决定的页面...

怎么在JS中使用Ajax进行异步请求?

大家好,今天我来分享一项JavaScript的实战技巧,即如何在JS中使用Ajax进行异步请求,让你的网页速度瞬间提升。Ajax是一种在不刷新整个网页的情况下与服务器进行数据交互的技术,可以实现异步加...

中小企业如何组建,管理团队_中小企业应当如何开展组织结构设计变革

前言写了太多关于产品的东西觉得应该换换口味.从码农到架构师,从前端到平面再到UI、UE,最后走向了产品这条不归路,其实以前一直再给你们讲.产品经理跟项目经理区别没有特别大,两个岗位之间有很...

前端监控 SDK 开发分享_前端监控系统 开源

一、前言随着前端的发展和被重视,慢慢的行业内对于前端监控系统的重视程度也在增加。这里不对为什么需要监控再做解释。那我们先直接说说需求。对于中小型公司来说,可以直接使用三方的监控,比如自己搭建一套免费的...

Ajax 会被 fetch 取代吗?Axios 怎么办?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!今天给大家带来的主题是ajax、fetch...

前端面试题《AJAX》_前端面试ajax考点汇总

1.什么是ajax?ajax作用是什么?AJAX=异步JavaScript和XML。AJAX是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX可以使网页实...

Ajax 详细介绍_ajax

1、ajax是什么?asynchronousjavascriptandxml:异步的javascript和xml。ajax是用来改善用户体验的一种技术,其本质是利用浏览器内置的一个特殊的...

6款可替代dreamweaver的工具_替代powerdesigner的工具

dreamweaver对一个web前端工作者来说,再熟悉不过了,像我07年接触web前端开发就是用的dreamweaver,一直用到现在,身边的朋友有跟我推荐过各种更好用的可替代dreamweaver...

我敢保证,全网没有再比这更详细的Java知识点总结了,送你啊

接下来你看到的将是全网最详细的Java知识点总结,全文分为三大部分:Java基础、Java框架、Java+云数据小编将为大家仔细讲解每大部分里面的详细知识点,别眨眼,从小白到大佬、零基础到精通,你绝...

福斯《死侍》发布新剧照 &quot;小贱贱&quot;韦德被改造前造型曝光

时光网讯福斯出品的科幻片《死侍》今天发布新剧照,其中一张是较为罕见的死侍在被改造之前的剧照,其余两张剧照都是死侍在执行任务中的状态。据外媒推测,片方此时发布剧照,预计是为了给不久之后影片发布首款正式预...

2021年超详细的java学习路线总结—纯干货分享

本文整理了java开发的学习路线和相关的学习资源,非常适合零基础入门java的同学,希望大家在学习的时候,能够节省时间。纯干货,良心推荐!第一阶段:Java基础重点知识点:数据类型、核心语法、面向对象...

不用海淘,真黑五来到你身边:亚马逊15件热卖爆款推荐!

Fujifilm富士instaxMini8小黄人拍立得相机(黄色/蓝色)扫二维码进入购物页面黑五是入手一个轻巧可爱的拍立得相机的好时机,此款是mini8的小黄人特别版,除了颜色涂装成小黄人...

2025 年 Python 爬虫四大前沿技术:从异步到 AI

作为互联网大厂的后端Python爬虫开发,你是否也曾遇到过这些痛点:面对海量目标URL,单线程爬虫爬取一周还没完成任务;动态渲染的SPA页面,requests库返回的全是空白代码;好不容易...

最贱超级英雄《死侍》来了!_死侍超燃

死侍Deadpool(2016)导演:蒂姆·米勒编剧:略特·里斯/保罗·沃尼克主演:瑞恩·雷诺兹/莫蕾娜·巴卡林/吉娜·卡拉诺/艾德·斯克林/T·J·米勒类型:动作/...

停止javascript的ajax请求,取消axios请求,取消reactfetch请求

一、Ajax原生里可以通过XMLHttpRequest对象上的abort方法来中断ajax。注意abort方法不能阻止向服务器发送请求,只能停止当前ajax请求。停止javascript的ajax请求...