CSS 动画的性能极限测试:同时驱动 1000 个元素的方法论
CSS 动画的性能极限测试同时驱动 1000 个元素的方法论一、写了一个 Demo屏幕上 1000 个彩色圆点各自以不同频率缩放跳动——FPS 从 60 掉到 8动画的构建过程很有仪式感用for循环生成 1000 个div给每个div分配随机的animation-delay和animation-duration让它们不完全同步营造一种有机呼吸感。在 Chrome DevTools 里打开 FPS Meter绿色的 60 稳稳的——前提是窗口大小 800×600 且没有别的 DOM。当把窗口拉到 2560×1440日常外接显示器尺寸、页面上还有 200 个文字节点时绿线瞬间断崖式下坠到 8fps。Performance 面板里一帧的主线程耗时 187ms16.67ms 的预算花掉了 11 倍。热点分布Recalculate Style 72ms、Layout 64ms、Paint 38ms、Composite 13ms。瓶颈不是 GPU 也不是 JavaScript而是 1000 个元素的animation属性每帧都在触发 CSS 样式的重新计算——每个元素的transform值变化都在样式计算阶段被浏览器遍历一遍匹配树。堆成 1000 倍72 毫秒就没了。要让 1000 个元素同时在屏幕上动而且 60fps需要走一条绕过样式计算的路径。二、为什么 1000 个 CSS Animation 比 100 个卡了不止 10 倍——非线性的瓶颈在样式计算flowchart TD subgraph 单个 CSS Animation 的帧流程 A1[animation 帧触发] -- B1[Recalculate Stylebr/样式计算] B1 -- C1[Layoutbr/布局] C1 -- D1[Paintbr/绘制] D1 -- E1[Compositebr/合成] end subgraph 1000 个 CSS Animation A2[1000 个 animationbr/同时触发帧] -- B2[Recalculate Stylebr/≈ 72ms] B2 -- C2[Layoutbr/≈ 64ms] C2 -- D2[Paintbr/≈ 38ms] D2 -- E2[Compositebr/≈ 13ms] E2 -- F2[帧总耗时 ≈ 187msbr/FPS ≈ 5] end subgraph 优化后GPU 合成器驱动 A3[CSS property 注册br/ transformbr/ will-change] -- B3[跳过 Stylebr/跳过 Layoutbr/跳过 Paint] B3 -- C3[Composite onlybr/≈ 2-4ms] C3 -- D3[帧总耗时 ≈ 4msbr/FPS ≈ 60] end1000 个 CSS Animation 和 100 个不是线性关系——CSS 样式计算引擎在处理大量具有animation属性的元素时需要为每个动画帧重新计算匹配规则、检查继承链、更新计算值。这个过程的时间复杂度接近 O(N × M)N 是动画元素数M 是规则表的复杂度。当 N 1000 时即使 M 很小也是沉重的开销。三、突破方案一CSS Paint APIHoudini——在合成器线程生成视觉/** * 方案对比矩阵1000 个元素 2560×1440 分辨率 * * ┌──────────────────────┬────────┬──────────┬────────────┐ * │ 方案 │ 平均FPS│ 帧耗时(ms)│ 主线程占用 │ * ├──────────────────────┼────────┼──────────┼────────────┤ * │ CSS animation×1000 │ 5 fps │ 187 │ 90% │ * │ CSS animation GPU │ 35 fps │ 28 │ 45% │ * │ RequestAnimationFrame│ 42 fps │ 23 │ 38% │ * │ CSS Paint API │ 58 fps │ 16 │ 8% │ * │ Canvas Offscreen │ 60 fps │ 12 │ 5% │ * └──────────────────────┴────────┴──────────┴────────────┘ */ /** * CSS Paint API 方案在 Paint Worklet 中绘制 1000 个圆点 * * 核心优势 * - Paint Worklet 运行在独立的 Worklet 线程不在主线程 * - 绘制到离屏位图后直接交给合成器——跳过主线程的 Style/Layout/Paint * - 每个元素的样式计算不再需要主线程解放 */ // 1. 注册 Paint Worklet // paint-worker.js class BouncingDotsPainter { /** * CSS Paint API 要求实现 static get inputProperties() * 声明需要哪些 CSS 属性作为输入 */ static get inputProperties() { return [ --dot-count, // 元素数量 --dot-base-size, // 基础尺寸 --dot-colors, // 颜色列表逗号分隔的 HEX 值 --animation-time, // 动画时间秒由 CSS animation 通过 property 传入 ]; } /** * 核心绘制函数 * param ctx Canvas 2D 上下文注意是 Worklet 的受限 Canvas * param size 绘制区域尺寸 * param props inputProperties 对应的 CSS 属性值 Map */ paint( ctx: PaintRenderingContext2D, size: { width: number; height: number }, props: Mapstring, CSSUnparsedValue, ) { const count parseInt(props.get(--dot-count)?.toString() || 100); const baseSize parseInt(props.get(--dot-base-size)?.toString() || 20); const colorsStr props.get(--dot-colors)?.toString() || #3B82F6,#EF4444,#10B981; const colors colorsStr.split(,); // 从 CSS 变量获取动画时间由 CSS animation 驱动每帧更新 const timeStr props.get(--animation-time)?.toString() || 0; const time parseFloat(timeStr); // 绘制 1000 个圆点每个根据 time seed 计算独立的位置和大小 for (let i 0; i count; i) { // 用素数种子生成伪随机确保每次渲染一致性 const seed (i * 2654435761) 0; // Knuth 乘法哈希 // 位置在画布内循环移动 const x ((seed % size.width) Math.sin(time * 1.3 i * 0.7) * 50 size.width) % size.width; const y ((seed % size.height) Math.cos(time * 0.9 i * 1.1) * 50 size.height) % size.height; // 大小随 time 缩放 const scale 0.5 0.5 * Math.sin(time * 2.0 i * 0.3); const radius (baseSize * scale) / 2; // 颜色循环使用颜色数组 const color colors[i % colors.length]; ctx.fillStyle color; ctx.globalAlpha 0.8; ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.fill(); } } } // 注册到全局 Paint Worklet 注册表 // ts-ignore registerPaint(bouncing-dots, BouncingDotsPainter); // 2. HTML 中使用只需要 1 个 div 元素 // style // property --animation-time { // syntax: number; // initial-value: 0; // inherits: false; // } // // .dots-canvas { // width: 100%; // height: 100vh; // background: paint(bouncing-dots); // --dot-count: 1000; // --dot-base-size: 20; // --dot-colors: #3B82F6,#EF4444,#10B981,#F59E0B,#8B5CF6; // /* 只有一个 animation不在元素层面重复 */ // animation: tick 10s linear infinite; // } // // keyframes tick { // to { --animation-time: 10; } // } // /style // div classdots-canvas/div // // 关键页面上只有 1 个元素1000 个圆点在 Worklet 里绘制。 // 主线程不需要计算 1000 个元素的样式→帧耗时从 187ms → 16ms。 // 3. 加载 Paint Worklet async function loadPaintWorklet() { if (paintWorklet in CSS) { // ts-ignore await CSS.paintWorklet.addModule(/paint-worker.js); } else { console.warn(CSS Paint API 不可用降级到 Canvas 方案); } }三续、突破方案二OffscreenCanvas Worker——纯 JavaScript 的 60fps 方案/** * OffscreenCanvas Web Worker 方案 * * 适用于 CSS Paint API 不支持的浏览器Firefox 直到 2025 年仍不支持 * 或者需要更精细的交互控制点击检测、拖拽等 */ // ─── 主线程代码 ────────────────────────────────────── class OffscreenDotsRenderer { private canvas: HTMLCanvasElement; private worker: Worker; private rafId: number 0; constructor(canvas: HTMLCanvasElement) { this.canvas canvas; // 1. 将 Canvas 控制权转移给 Worker const offscreen canvas.transferControlToOffscreen(); // 2. 创建 Worker注意vite/webpack 需要特殊配置导入 worker this.worker new Worker( new URL(./dots-worker.ts, import.meta.url), { type: module }, ); // 3. 把 OffscreenCanvas 传给 Worker零拷贝转移所有权 this.worker.postMessage( { type: init, canvas: offscreen, width: canvas.clientWidth * devicePixelRatio, height: canvas.clientHeight * devicePixelRatio, dpr: devicePixelRatio, }, [offscreen], // 转移所有权 ); // 4. 监听 resize 变化通知 Worker 重设尺寸 const resizeObserver new ResizeObserver(entries { for (const entry of entries) { const { width, height } entry.contentRect; this.worker.postMessage({ type: resize, width: width * devicePixelRatio, height: height * devicePixelRatio, }); } }); resizeObserver.observe(canvas); // 5. 主线程的渲染循环——只发消息不画图 const loop (timestamp: number) { this.worker.postMessage({ type: frame, timestamp }); this.rafId requestAnimationFrame(loop); }; this.rafId requestAnimationFrame(loop); } destroy() { cancelAnimationFrame(this.rafId); this.worker.terminate(); } } // ─── Worker 线程代码 (dots-worker.ts) ───────────────── interface Dot { x: number; y: number; radius: number; color: string; phase: number; // 动画相位0-2π产生错落感 speed: number; // 动画速度0.5-2.0 vx: number; // 每帧 x 速度 vy: number; // 每帧 y 速度 } let canvas: OffscreenCanvas | null null; let ctx: OffscreenCanvasRenderingContext2D | null null; let dots: Dot[] []; let width 0; let height 0; let lastTimestamp 0; self.onmessage (e: MessageEvent) { const { type } e.data; switch (type) { case init: { canvas e.data.canvas; width e.data.width; height e.data.height; ctx canvas!.getContext(2d)!; // 初始化 1000 个圆点只做一次 dots createDots(1000, width, height); break; } case resize: { width e.data.width; height e.data.height; if (canvas) { canvas.width width; canvas.height height; } // resize 后重新初始化圆点让它们适配新画布 dots createDots(1000, width, height); break; } case frame: { const dt (e.data.timestamp - lastTimestamp) / 1000; // 秒 lastTimestamp e.data.timestamp; renderFrame(dt); break; } } }; /** * 一次性创建 1000 个圆点每个有独立的动画参数 * 不在每帧中创建数组→零 GC 压力 */ function createDots(count: number, w: number, h: number): Dot[] { const colors [#3B82F6, #EF4444, #10B981, #F59E0B, #8B5CF6]; const result: Dot[] []; for (let i 0; i count; i) { result.push({ x: Math.random() * w, y: Math.random() * h, radius: 5 Math.random() * 15, color: colors[Math.floor(Math.random() * colors.length)], phase: Math.random() * Math.PI * 2, speed: 0.5 Math.random() * 1.5, vx: (Math.random() - 0.5) * 2, // 每帧移动速度 vy: (Math.random() - 0.5) * 2, }); } return result; } /** * 每帧渲染在 Worker 线程中不影响主线程 * * 核心优化 * 1. 所有计算在 Worker 线程→主线程解放 * 2. 用 for 循环 预计算sin/cos 提前算好 phase*t * 3. 透明度用 ctx.globalAlpha 做乘法比修改 fillStyle 快 */ function renderFrame(dt: number) { if (!ctx || !canvas) return; // 清除画布不清除会有残影但清除也是昂贵的——考虑 dirty rect ctx.clearRect(0, 0, width, height); const t performance.now() / 1000; // 预计算当前帧的全局缩放batch 相同颜色的 dot 一起画可进一步优化 for (let i 0; i dots.length; i) { const dot dots[i]; // 位置循环移动 正弦波动 dot.x dot.vx; dot.y dot.vy; if (dot.x 0) dot.x width; if (dot.x width) dot.x 0; if (dot.y 0) dot.y height; if (dot.y height) dot.y 0; // 缩放基于 phase speed 的独立波浪 const scale 0.6 0.4 * Math.sin(t * dot.speed dot.phase); const r dot.radius * scale; ctx.beginPath(); ctx.arc(dot.x, dot.y, r, 0, Math.PI * 2); ctx.fillStyle dot.color; ctx.globalAlpha 0.7 0.3 * Math.sin(t * 1.5 dot.phase); ctx.fill(); } // 重置 alpha ctx.globalAlpha 1; // ── 可选优化如果 dot 数量达到 5000在 Worker 中做 dirty rect ── // 只重绘有变化的区域而非全画布 clearRect }四、边界分析极限测试中的非预期性能瓶颈4.1 屏幕分辨率的指数增长效应2560×1440 面积是 1920×1080 的 1.78 倍——但 Canvas 渲染的性能损耗不是线性 1.78 倍而是接近 2.5-3 倍。原因Canvas 内部使用 4 字节/像素的 RGBA Buffer2560×1440 14.75MB 的帧缓冲。clearRect 在这个尺度的帧缓冲上每秒执行 60 次 885MB/s 的带宽。如果用户还开了 2x 缩放常见 Mac实际是 5120×2880 59MB 帧缓冲——这个带宽要求超过了部分集成显卡的吞吐能力。极限测试的硬件环境非常重要在 M1 Pro 上 60fps 的方案在 Intel UHD 620 可能只有 25fps。4.2 CSS Paint API 的可用性困境paintWorklet目前只在 Chromium 浏览器Chrome/Edge/Opera中可用Safari 不支持Firefox 也不支持。覆盖率约 75%。如果你用supports (background: paint(id))做渐进增强不支持的话你仍然需要一个降级方案Canvas Worker——结果是你要维护两套实现。很多团队的理性选择是直接用 Canvas 方案跳过 Paint API。4.3 Worker 线程的调试地狱OffscreenCanvas 在 Worker 中运行时Chrome DevTools 的 Performance 面板可以捕获 Worker 的帧时间线2024 年已支持但 Elements、Styles 面板完全看不到 Worker 中的内容。调试 1000 个圆点的绘制逻辑时你不能像 inspect 一个 div 那样看它的计算样式。只能靠console.log和画 debug 色块——调试效率低了一个数量级。4.4 交互能力的严重退化paint(worklet)和 OffscreenCanvas 画出来的都是像素——不是 DOM 节点。800 个 div 每个都可以绑定 onClick、onHover、CSS 过渡Canvas 里的 800 个圆点交互需要手动做 hit-testing判断鼠标坐标落在哪个圆形内。如果你需要丰富的交互——hover 高亮、点击选中、右键菜单——用 Canvas 的开发成本远高于用 CSS DOM。适用边界CSS Paint API 适合纯视觉装饰性元素的批量绘制如背景粒子、水波纹、星空效果不需要交互不需要支持非 Chromium 浏览器。OffscreenCanvas Worker 适合有复杂绘图计算但不交互或简单交互的场景需要在所有现代浏览器运行。当需要富交互时用 requestAnimationFrame 分批渲染每帧只计算可见区域的 50 个元素比 Worker Canvas 的性价比更高。五、总结1000 个 CSS animation 同时运行时瓶颈不在 GPU 也不在 JS——在样式重计算Recalculate Style ≈ 72ms占帧耗时 40%| 帧流程瓶颈分布Recalculate Style72ms Layout64ms Paint38ms Composite13ms | CSS Paint API 在 Paint Worklet 线程绘制用 1 个元素承载 1000 个虚拟元素主线程只发一个 property 帧信号 | CSS property 注册--animation-time为number类型→CSS animation 驱动属性变化→paint() 回调读取新值 | OffscreenCanvas WorkertransferControlToOffscreen() 转移 Canvas 所有权→主线程只 postMessage 时间戳→Worker 中每帧绘制 1000 个圆点 | Worker 中预创建 Dot[] 数组1000 个对象每帧只更新位置和缩放→零 GC 压力 | 2560×1440 的 Canvas 帧缓冲 14.75MB2x 59MB——clearRect 每秒 60 次 3.5GB/s 带宽对集成显卡构成压力 | Paint API 只 Chromium 可用≈75% 覆盖率需要 fallback 到 Canvas 方案→维护两套代码 | Canvas 方案的交互能力退化800 个圆点需要手动 hit-testing数学距离判断不像 div 能直接绑 onClick | 富交互场景hover/click/drag用 DOM rAF 分批渲染每帧计算可见区域而不是全局比 Canvas 更划算。

相关新闻

AI 驱动的视觉回归测试:像素级差异的智能分析与误报过滤

AI 驱动的视觉回归测试:像素级差异的智能分析与误报过滤

AI 驱动的视觉回归测试:像素级差异的智能分析与误报过滤 一、CI 中 217 个截图差异报告全被点了 "Approve"——不是代码没问题,是谁都不想看 217 个对比图 一个 UI 组件库升级了依赖版本后,视觉回归测试(Visual Regress…

2026/7/15 22:00:21阅读更多 →
华为MetaERP PO模块集成方案设计时主要关注入站接口(Inbound/导入)、出站出口(Outbound/报表或BI抽取)、关联模块接口(接收/请购/AP)以及PL/SQL API。下面按架构视

华为MetaERP PO模块集成方案设计时主要关注入站接口(Inbound/导入)、出站出口(Outbound/报表或BI抽取)、关联模块接口(接收/请购/AP)以及PL/SQL API。下面按架构视

PO模块集成方案设计时主要关注入站接口(Inbound/导入)、出站出口(Outbound/报表或BI抽取)、关联模块接口(接收/请购/AP)以及PL/SQL API。下面按架构视角分类说明 Oracle EBS R12 PO模块的标准接口。一、PO采…

2026/7/15 22:00:21阅读更多 →
链上 AI 模型缓存层设计:推理结果复用、热度淘汰策略与存储成本优化

链上 AI 模型缓存层设计:推理结果复用、热度淘汰策略与存储成本优化

链上 AI 模型缓存层设计:推理结果复用、热度淘汰策略与存储成本优化 一、链上 AI 的推理成本危机:为什么每次推理都不应该是新请求 链上 AI(On-chain AI)正在从概念验证走向生产应用。无论是用 ZKML 验证模型推理的正确性&#xf…

2026/7/15 22:00:21阅读更多 →
基于MATLAB与机器人工具箱的六自由度机械臂笛卡尔空间轨迹规划实战

基于MATLAB与机器人工具箱的六自由度机械臂笛卡尔空间轨迹规划实战

1. 从零搭建UR5e机械臂模型第一次接触机械臂建模时,我被DH参数表绕得头晕眼花,直到发现MATLAB机器人工具箱的Link函数就像乐高积木说明书。以UR5e为例,我们先把六个关节的"身份证信息"整理好:% 标准DH参数定义UR5e机械臂…

2026/7/15 23:05:32阅读更多 →
K8s PV 动态供给的陷阱:StorageClass 参数配错后的连锁反应

K8s PV 动态供给的陷阱:StorageClass 参数配错后的连锁反应

K8s PV 动态供给的陷阱:StorageClass 参数配错后的连锁反应 一、一个 StorageClass 的 reclaimPolicy: Delete 差点让整个数据库没了 故事很简单:运维创建了一个 MySQL StatefulSet,PVC 使用默认 StorageClass,一切正常。三个月…

2026/7/15 23:05:32阅读更多 →
从选型到焊接:阻容封装尺寸速查与实战应用指南

从选型到焊接:阻容封装尺寸速查与实战应用指南

1. 阻容封装尺寸入门:为什么0201和0402不只是数字第一次接触电阻电容的封装尺寸时,我也被那些数字搞晕过——0201、0402、0603,看起来像某种密码。直到有次手工焊接时错把0603当成0402,烙铁头压下去的瞬间,那个比芝麻还…

2026/7/15 23:05:32阅读更多 →
从教育焦虑到个性化学习:破解孩子学习动力的密码

从教育焦虑到个性化学习:破解孩子学习动力的密码

1. 从“催命符”到成长阶梯:理解教育焦虑背后的真实诉求“知道朱尼尔拿的是书,不知道的以为他拿的是催命符”——这句网络热梗精准戳中了当代家长的教育焦虑。表面看是调侃孩子对学习的抗拒,深层反映的却是家庭教育中普遍存在的压力传递现象。…

2026/7/15 23:05:32阅读更多 →
第四章 | 网络层核心概念精解与实战习题剖析(谢希仁第八版)

第四章 | 网络层核心概念精解与实战习题剖析(谢希仁第八版)

1. IP地址规划与子网划分实战IP地址规划是网络层设计的核心基础,就像给城市规划门牌号一样重要。我们先从一个实际案例入手:假设你拿到一个地址块136.23.12.64/26,现在需要为市场部(60台主机)、技术部(30台…

2026/7/15 23:05:32阅读更多 →
计算机毕业设计之基于springboot的助农平台的设计与实现

计算机毕业设计之基于springboot的助农平台的设计与实现

随着新经济的需求和新技术的发展,特别是网络技术的发展,如果可以建立起助农平台,就可以改变传统线下管理方式。由于中国存在很多的企业或者学校等等,目前为止仍然采用纸质化管理方式,整体管理方式相对落后。该种方式不…

2026/7/15 23:00:32阅读更多 →
VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异

VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异

VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异当你在VSCode中启动一个新的TypeScript项目时,第一个技术决策往往从安装方式开始。这个看似简单的选择——全局安装还是项目本地安装——实际上会深刻影响你的开发流程、团队协作和…

2026/7/15 6:42:19阅读更多 →
智慧树刷课插件:5分钟实现自动化学习的智能助手

智慧树刷课插件:5分钟实现自动化学习的智能助手

智慧树刷课插件:5分钟实现自动化学习的智能助手 【免费下载链接】zhihuishu 智慧树刷课插件,自动播放下一集、1.5倍速度、无声 项目地址: https://gitcode.com/gh_mirrors/zh/zhihuishu 智慧树刷课插件是一款专为智慧树在线教育平台设计的Chrome浏…

2026/7/15 6:12:45阅读更多 →
Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案

Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案

Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案 【免费下载链接】WorkshopDL WorkshopDL - The Best Steam Workshop Downloader 项目地址: https://gitcode.com/gh_mirrors/wo/WorkshopDL 你是否在GOG或Epic Games Store购买了心仪的游戏…

2026/7/15 10:54:00阅读更多 →
AI框架决定企业AI能走多远

AI框架决定企业AI能走多远

企业AI建设的第一性原理 企业搞AI,最关键的决定是什么?不是选哪家大模型,不是先做哪个场景,不是招多少AI人才——而是选哪个AI开发框架。 为什么?因为框架决定了企业AI能力的"天花板"。选对了框架&#xff0…

2026/7/15 0:01:30阅读更多 →
Java企业为什么需要AI框架

Java企业为什么需要AI框架

Java企业在AI时代的尴尬处境 Java是全球企业级应用开发的主流语言——全球超过一半的企业系统跑在Java上。但在AI浪潮面前,很多Java企业感到尴尬:大模型的接口是各种语言的,AI开发社区以其他语言为主流,似乎Java在AI时代"掉队…

2026/7/15 0:01:30阅读更多 →
CC3230x嵌入式开发实战:SD主机、定时器与低功耗模式深度解析

CC3230x嵌入式开发实战:SD主机、定时器与低功耗模式深度解析

1. 项目概述:为什么需要关注CC3230x的SD主机、定时器与低功耗?在物联网和嵌入式设备开发领域,我们常常面临一个核心矛盾:设备需要具备强大的连接能力、可靠的数据存储和实时控制功能,同时又必须严格控制功耗以延长电池…

2026/7/15 0:01:30阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/15 15:50:47阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/15 8:52:38阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/15 14:06:23阅读更多 →