简介PDF.js是一款由Mozilla团队开发的开源JavaScript库无需安装额外插件就能在浏览器中解析并渲染PDF文档。这份调试可用版资源面向需要在网站或应用中集成PDF在线预览功能的前端开发者支持IE、Firefox、Chrome等多浏览器解决用户必须下载阅读器才能查看文件的痛点。资源压缩包为1.7MB共367个文件包含核心JavaScript脚本、HTML入口、CSS样式以及bcmap字符映射表、properties配置、png图标等辅助文件目录结构清晰便于直接部署到项目中调试运行。得益于HTML5 Canvas技术它通过流式加载和分块渲染策略处理大文件支持获取页数、元数据、定制界面等丰富API。调试版本带有更详尽的日志和错误检测输出能帮助开发者快速定位渲染不一致、内存占用等问题。当前已有1085人学习或下载适合有一定JavaScript基础、正在搭建文档预览服务的开发人员作为可直接复用的参考实现。1. pdf.js在线查看PDF工具从白屏投诉到换掉渲染方案说到 pdf.js在线查看pdf工具很多人的第一反应是先抱一个现成的开源查看器配好静态服务直接嵌进 iframe。我一开始也这么干直到某次给某客户做文档管理系统时被“PDF 一直转圈、页面白屏、刷新也没用”的反馈折磨了两天才真正去研究这个库到底按什么链路在跑。pdf.js 的价值就一句话把 PDF 当成能被 JavaScript 解析和绘制的数据源而不是让浏览器去“打开一个文件”。它能解决的是网页内 PDF 预览的整套体验——翻页、缩放、旋转、文本选择、搜索、打印都能嵌在业务页面里做。这篇文章适合正在做文档系统、审批流、在线课堂、档案预览的开发者无论是想快速验证还是深度集成都能找到可复现的路径。2. pdf.js 的渲染原理与两条起步路径官方查看器和 API 封装怎么选2.1 浏览器原生预览与 pdf.js 的分工为什么在线预览要自己搭先说结论pdf.js 在在线预览领域基本是事实标准你很难找出第二个活跃度相当、能自己控制渲染过程的方案。它的设计和浏览器内置阅读器不同它把自己拆成 core、display、worker 三层。core 层负责解析 PDF 的二进制结构——文件头、xref 交叉引用表、对象树、字体表、页面内容流display 层负责把内容流里的绘图指令翻译成 Canvas 2D 的 API 调用worker 层则负责在后台线程跑 core 的解析任务避免主线程被阻塞。这个分层决定了以后排错的方向请求拿不到文件就查网络层解析失败就查字体和 xref画出来空白就查渲染层。那什么时候不需要自己搭如果只是把 PDF 放在公网或者内部站点上让用户点开一个链接浏览器自带的阅读器就能直接显示。但浏览器内置阅读器对开发者来说基本是黑匣子你拿不到渲染进度也定制不了工具栏更谈不上把文本内容抽取出来做检索或关键词高亮。一旦预览功能要嵌进管理系统、要限制下载、要跟审批表单联动原生方案就不太够用了。在线查看工具这个需求本质上是“要在自己的页面里控制 PDF 的一切”这正是 pdf.js 存在的理由。2.2 最短路径验证把官方 viewer 跑起来先别急着写代码。下载 pdf.js 的 release 包不是源码仓库而是带构建产物的发布包解压后你会看到 web 和 build 两个目录。web 目录下有官方做好的 viewer.htmlbuild 目录下有库主文件和 worker 文件。对于第一次接触这个方案的人我的习惯是先不碰代码把官方查看器跑起来用一次最小验证确认环境没有坑。# 假设 release 包已经解压到当前目录下的 pdfjs-demo 目录 # 用任意静态文件服务起一个本地端口 python3 -m http.server 8080 --directory ./pdfjs-demo # 浏览器访问下面的地址 # http://localhost:8080/web/viewer.html提示直接用 file:// 协议双击打开 viewer.html会被浏览器的跨域策略拦死必须通过 http 服务访问。官方 viewer 跑通之后再看下面这个带参数的地址。它让你在不解压任何代码的情况下把指定 PDF 喂给查看器用来快速确认“这个 PDF 文件本身能不能被解析”。# 假设放一个 test.pdf 在静态服务根目录 # http://localhost:8080/web/viewer.html?file/test.pdf如果能看到 PDF 正常渲染、能翻页缩放说明你下载的 release 包、静态服务、网络链路都是对的。后面自己封装渲染层时再出现白屏就能把怀疑范围缩小到代码层而不是环境层。很多新人卡在第一关就是因为没做这个最小验证直接用框架去封装出了问题一步都排不下去。2.3 官方 viewer 和自研渲染层一张选型决策表官方 viewer.html 是“拿来即用”的完整产品缩放、打印、搜索、侧栏都做好了适合给内部工具、演示系统做快速落地。坏处是 UI 不好改官方皮肤和 DOM 结构耦合得比较深想藏掉某个按钮都要翻半天样式。自己用 pdf.js API 写渲染层代价是工作量大但交互完全可控适合把 PDF 预览嵌到业务表单、审批流、在线签章这类场景里。对比项直接用官方 viewer自研渲染层开发成本极低配静态服务即可取决于功能深度通常按人日计算UI 定制难官方 DOM 结构和样式耦合完全可控文本选择/搜索开箱即用需要自己接 TextLayer与业务系统联动难事件回调不直观可以完全按业务设计维护成本升级官方包即可需要自己跟进 API 变化我一般会这样给建议如果你只是“要有个地方能看 PDF”直接用官方 viewer不要再造轮子如果你要把 PDF 塞进自己的业务页面还要加水印、签名、审批痕迹、指定页跳转就走 API 封装。前者的交付时间是小时级后者是按天算。后面几章的内容都以 API 封装为主线展开因为这是大多数在线查看工具真正要落地的方向。3. 用 pdf.js API 把 PDF 渲染进业务页面最小链路、进度反馈与封装3.1 最小渲染链路getDocument 到 getPage 再到 render自研渲染层的第一步是把 PDF 的第一页画到 canvas 上。这个链路固定三步getDocument 拿文档对象getPage 拿指定页page.render 画到画布。下面的代码是完整的最小示例用到了 ES Module 的导入方式也可以直接通过 script 标签引入后使用全局 pdfjsLib 对象。import * as pdfjsLib from pdfjs-dist; // 必须指定 worker 入口否则 pdf.js 会在主线程做解析页面会卡死 pdfjsLib.GlobalWorkerOptions.workerSrc /js/pdfjs/pdf.worker.min.mjs; async function renderFirstPage(url, canvas) { // getDocument 接收 URL 字符串或 Uint8Array const loadingTask pdfjsLib.getDocument(url); const pdf await loadingTask.promise; // 文档对象包含页数、元数据 const page await pdf.getPage(1); // 页码从 1 开始不是 0 const viewport page.getViewport({ scale: 1 }); // 缩放系数1 表示原始尺寸 canvas.width viewport.width; canvas.height viewport.height; const ctx canvas.getContext(2d); // render 返回 RenderTask可以监听 promise 也可以主动 cancel const renderTask page.render({ canvasContext: ctx, viewport: viewport }); await renderTask.promise; }逻辑说明pdf.js 的渲染完全依赖 viewport 对象canvas 的宽高必须和 viewport 对齐否则画面会被裁切或者留白。页面内容流里的绘图指令执行完后render 的 promise 才会 resolve所以后续要做“渲染完成后再显示”之类的逻辑必须 await renderTask.promise。如果渲染中途切页直接启动下一个 render 会互相干扰正确做法是先调 renderTask.cancel() 再画新页。参数说明workerSrc 的路径写错是高频问题后面避坑章会展开。这里的 scale 参数也别急着设成 1因为在高分屏上逻辑像素和物理像素有差别直接设 1 会糊下一章会给出配套的处理方案。getDocument 可以传 URL string也能传 Uint8Array业务上如果 PDF 需要带 Token 鉴权我通常会先自己 fetch 拿到 ArrayBuffer再转 Uint8Array 传给 pdf.js这样鉴权逻辑留在业务层渲染层保持干净。3.2 加载进度用 onProgress 做真正可用的进度条很多在线预览工具的“加载中”是假的一个转圈图标转到底。pdf.js 其实给了真实的进度事件监听 onProgress 就行。这里有一个细节容易出错如果服务的响应头里没有 Content-Length或者后端用 chunked 编码progressData.total 会是 0算出来的百分比永远是 NaN。const loadingTask pdfjsLib.getDocument({ url: fileUrl, // 有需要可以加 disableAutoFetch 之类的参数 }); loadingTask.onProgress (progressData) { // progressData.loaded 是已加载字节数 // progressData.total 是 PDF 总字节数 if (progressData.total 0) { const percent Math.round((progressData.loaded / progressData.total) * 100); console.log(PDF 下载进度${percent}%); } };参数说明onProgress 覆盖的是“文件下载”阶段不是渲染阶段。很多新手在这里有个误解以为进度到 100% 页面就出来了实际上大 PDF 的渲染耗时同样明显尤其是页面里嵌了大量矢量图和透明混合模式时canvas 绘制可能比网络下载还慢。所以我一般会把进度拆成两段下载进度用 onProgress渲染进度则自己按“已渲染页数/总页数”上报这样用户在打开大文件时看到的是一个分段推进的完整过程。如果后端没给总长度一个常见的兜底做法是显示“已加载 X MB”而不是假装百分比至少用户知道它还在动。3.3 把渲染链封装成在线查看器的核心类最小链路跑通后下一步是封装。我习惯写一个 PdfViewer 类把 canvas 尺寸、缩放、当前页、渲染任务都收进去。这个类看起来简单但它决定了后面所有交互是不是好加。核心是渲染逻辑只接受页码和缩放系数内部自己处理高清屏适配和渲染任务切换。class PdfViewer { constructor(canvas, options {}) { this.canvas canvas; this.scale options.scale || 1; // 基础缩放 this.pageIndex 0; // 当前页码 this.pdf null; this.renderTask null; } async open(url) { const loadingTask pdfjsLib.getDocument(url); this.pdf await loadingTask.promise; await this.renderPage(1); } async renderPage(pageNumber) { // 上一页没渲染完先取消避免两个 render 抢同一个 canvas if (this.renderTask) { this.renderTask.cancel(); } const page await this.pdf.getPage(pageNumber); const viewport page.getViewport({ scale: this.scale }); // 高清屏处理canvas 物理尺寸按 devicePixelRatio 放大CSS 尺寸保持逻辑值 const dpr window.devicePixelRatio || 1; this.canvas.width Math.floor(viewport.width * dpr); this.canvas.height Math.floor(viewport.height * dpr); this.canvas.style.width ${viewport.width}px; this.canvas.style.height ${viewport.height}px; const ctx this.canvas.getContext(2d); // transform 参数把渲染坐标系放大 dpr 倍保证线宽和文字不发虚 const transform dpr 1 ? null : [dpr, 0, 0, dpr, 0, 0]; const renderTask page.render({ canvasContext: ctx, viewport, transform }); this.renderTask renderTask; await renderTask.promise; // 渲染完成后释放页面占用的资源 page.cleanup(); this.pageIndex pageNumber; } async next() { if (this.pageIndex this.pdf.numPages) { await this.renderPage(this.pageIndex 1); } } async prev() { if (this.pageIndex 1) { await this.renderPage(this.pageIndex - 1); } } }这段代码里的 transform 是 Canvas 2D 的坐标变换矩阵用 [dpr, 0, 0, dpr, 0, 0] 把渲染坐标系放大 dpr 倍同时 canvas 元素在页面上的 CSS 尺寸保持 viewport 的逻辑宽高。这样画出来的图既清晰又不会把布局撑爆。page.cleanup() 的作用是释放这一页解析出来的字体和图像数据在长时间翻页的场景里非常关键不调用的话内存会随着翻页次数持续上涨最后表现为页面越来越卡。4. 在线查看工具的完整能力缩放旋转、文本选择与移动端适配4.1 把 canvas 和 PDF 坐标对齐scale、rotation 与 viewport 的关系在线查看工具不能只能固定看一页缩放和旋转是最基础的操作。这里关键是理解 viewport 是“一次性计算”的产物每次缩放或旋转都要重新调用 page.getViewport 拿到新参数再重新设置 canvas 的宽高。旋转 90 度或 270 度之后页面的宽高会互换如果还沿用旧的 canvas 尺寸画面就会被裁掉一截。function applyViewport(page, canvas, scale, rotation 0) { // rotation 必须是 90 的整数倍 const viewport page.getViewport({ scale, rotation }); const dpr window.devicePixelRatio || 1; canvas.width Math.floor(viewport.width * dpr); canvas.height Math.floor(viewport.height * dpr); canvas.style.width ${viewport.width}px; canvas.style.height ${viewport.height}px; const ctx canvas.getContext(2d); const transform dpr 1 ? null : [dpr, 0, 0, dpr, 0, 0]; return { viewport, ctx, transform }; }参数说明scale 是独立于浏览器缩放的一层系数它只影响 PDF 渲染的分辨率不影响 CSS 布局。所以做“放大镜”功能时不要用 CSS transform 去放大 canvas那会导致文字和线条模糊正确做法是提高 scale 后重新渲染。rotation 只接受 90 的整数倍设计上把旋转做成固定档位0/90/180/270比支持任意角度好得多因为任意角度会让 viewport 计算变得复杂交互也难控制。画布重新调整后之前渲染的内容会被清空要立刻触发新一次 page.render这其实算是个优点——它天然解决了旋转后残留脏数据的问题。4.2 文本选择与搜索TextLayer 的接入和错位修复PDF 在渲染成 canvas 之后文字本质上是像素浏览器无法选中和搜索。在线查看工具如果要支持“选中文字复制”和“关键词定位”必须叠加一层文本层TextLayer。原理是调用 page.getTextContent 拿到每个文字块的位置和内容再生成透明的 span 绝对定位在 canvas 上方。const textLayerDiv document.getElementById(text-layer); textLayerDiv.innerHTML ; const textContent await page.getTextContent(); // renderTextLayer 会把文字 span 加入 container await pdfjsLib.renderTextLayer({ textContentSource: textContent, container: textLayerDiv, viewport: viewport, textDivs: [] }).promise;配套的 CSS 也很关键缺了样式会看到文字叠在 PDF 图案上#text-layer { position: absolute; inset: 0; overflow: hidden; line-height: 1; } #text-layer span { position: absolute; color: transparent; transform-origin: 0 0; white-space: pre; }这里有一个高频坑缩放后文本层错位。因为 canvas 有 dpr 变换而 text-layer 的定位完全依赖 viewport 计算出来的坐标如果只重画了 canvas 没重新渲染文本层选中的高亮位置会和画面里的字对不上。正确顺序是每次 scale 变化时先重新计算 viewport然后重画 canvas再用同一个 viewport 去 renderTextLayer两个动作必须绑定。textDivs 数组可以复用但最简单可靠的方案是每次渲染前把容器清空重来。4.3 移动端适配触摸滚动、双指缩放与内存释放移动端是在线查看工具的另一个主战场。canvas 渲染本身不区分平台但交互差别很大。桌面端用滚轮和按钮移动端要靠触摸滚动和双指缩放。我一般会先做一个无脑方案外层容器 overflow:auto让单指滑动变成原生滚动然后提供 1x/2x 两档缩放按钮暂时不做手势缩放。原因很现实双指缩放和页面滚动同时存在时事件判定非常容易冲突用户一旦感觉“这页面不听使唤”体验就直接崩了。// 触摸滚动最简单可靠的实现交给浏览器原生滚动 // 只需要在容器上设置 // overflow: auto; // -webkit-overflow-scrolling: touch; // 双指缩放如果是刚需用 touch 事件自己计算 scale let lastDistance 0; container.addEventListener(touchstart, (e) { if (e.touches.length 2) { const dx e.touches[0].clientX - e.touches[1].clientX; const dy e.touches[0].clientY - e.touches[1].clientY; lastDistance Math.sqrt(dx * dx dy * dy); } }, { passive: true }); container.addEventListener(touchmove, (e) { if (e.touches.length 2) { const dx e.touches[0].clientX - e.touches[1].clientX; const dy e.touches[0].clientY - e.touches[1].clientY; const distance Math.sqrt(dx * dx dy * dy); const ratio distance / lastDistance; lastDistance distance; // 用 ratio 更新 scale然后重新渲染当前页 } }, { passive: true });移动端还有一个容易翻车的点iOS 系统浏览器对 canvas 数量和总内存有比较严格的限制一旦同时存在的 canvas 太多系统可能在后台直接把页面杀掉。做法是跑滚动懒加载只保留视口附近的几页离开视口的页面在 page.cleanup() 之外还要把 canvas 节点从 DOM 里移除或者把像素数据清掉。这个细节在桌面端影响不大在移动端是生死线后面避坑章会再展开。5. 在线 PDF 查看器避坑指南跨域、worker 路径与白屏排查5.1 现象PDF 请求一直失败控制台报跨域错误自研渲染层跑起来后最常见的第一个坑是 PDF 文件根本拉不回来。症状是控制台出现类似 “Access to fetch at ... from origin ... has been blocked by CORS policy” 的报错页面一直停留在加载状态。原因在于 pdf.js 默认用 fetch 去拉 PDF 二进制而 fetch 受浏览器的同源策略限制PDF 文件和前端页面不在同一个源时服务器必须返回明确的 CORS 响应头。解决方式有几种。最稳妥的是把 PDF 和前端部署在同一个源下通过反向代理把 PDF 路径转发到存储服务这也是我推荐的方案能一并解决 Token 鉴权的问题。如果确实要跨源需要在服务端给 PDF 响应加上跨域头// 服务端中间件示意给 PDF 请求补上 CORS 响应头 server.get(/pdf/*, (req, res, next) { res.setHeader(Access-Control-Allow-Origin, *); res.setHeader(Access-Control-Allow-Methods, GET, HEAD, OPTIONS); next(); });注意不要把这个配置写得太宽“*”适合公开文档如果 PDF 带敏感信息还是要配成明确的域名白名单。另外如果 PDF 是从带鉴权的接口拿的我一般不走这种方案而是前端先 fetch带上 Authorization 头拿 ArrayBuffer 后传给 getDocument这样跨域头就不是瓶颈了。5.2 现象打开页面直接白屏控制台提示 workerSrc 未指定第二个高频坑是 worker 入口配置错误。症状有两种控制台报 “No GlobalWorkerOptions.workerSrc specified”或者报一个奇怪的 “The worker was unable to load from the given path” 然后白屏。原因很简单pdf.js 的解析工作默认放在 worker 线程你必须告诉它 worker 脚本在哪。这个路径一旦配错或漏配整个解析流程起不来。// 标准配置路径的根是静态服务根目录 pdfjsLib.GlobalWorkerOptions.workerSrc /js/pdfjs/pdf.worker.min.mjs;这里特别容易踩的细节是文件后缀。老版本是 pdf.worker.js新版本走向 ES Module 之后变成了 pdf.worker.min.mjs两个后缀不是一个文件。如果你把网上老教程里的 .js 结尾直接抄过来在较新版本里同样报错。文件后缀以你下载的 release 包里的实际文件名为准最可靠的办法是打开 build 目录看一眼。另外需要确认这个路径必须能通过浏览器直接访问有人会把文件放在 src 目录里然后 import但 worker 脚本的路径是运行时拼接的不经过打包器处理所以放到 public 或静态目录下是最省心的。5.3 现象翻页翻到后面页面越来越卡最后直接崩溃大文件翻页导致崩溃是内存管理没做好。症状通常在 PDF 超过几十页之后出现前几页反应正常后面越翻越慢最后标签页直接崩掉。原因是每个页面渲染后pdf.js 内部会缓存字体、图像和解析出来的页面对象如果每一页渲染完都不释放这些对象会一直留在内存里。// 渲染完当前页后调用 page.cleanup() 释放资源 await renderTask.promise; page.cleanup();另一个做法是给渲染任务加 cancel 逻辑。翻页很快时上一页还没渲染完下一页的渲染请求就来了两个 render 同时操作同一个 canvas 会产生不可预期的画面和性能问题。这里我会做一个“渲染任务优先级”管理每页渲染启动时把旧任务 cancel 掉同时记录当前渲染的页码滚动过快时直接跳到最后的目标页。还有一点容易被忽略缩放时如果每次缩放都重新渲染旧的 canvas 内容虽然被覆盖但 GPU 内存不会立刻回收连续快速缩放多次同样可能卡顿可以考虑在缩放结束后加一个 100 毫秒的节流再触发渲染。5.4 现象中文变成方块或者个别字体显示成“豆腐块”字体渲染问题在中文 PDF 里尤其明显。现象是页面能显示但文字全成了方框或者缺字。原因通常是两种一是 PDF 里内嵌的字体子集没有被完整解析二是缺失的字体没有合适的系统字体兜底。pdf.js 默认依赖一些标准字体数据和 CMaps不同的发布包对中文字体的支持程度不一样。// 常见做法显式配置 CMaps 和标准字体数据路径 const loadingTask pdfjsLib.getDocument({ url, cMapUrl: /js/pdfjs/cmaps/, cMapPacked: true, standardFontDataUrl: /js/pdfjs/standard_fonts/, useSystemFonts: true });参数说明cMapUrl 指向字符映射表目录用于解析 PDF 内的 CID 字体standardFontDataUrl 指向标准字体数据useSystemFonts 表示允许在系统字体里查找匹配替代。这些路径和 worker 路径类似都是运行时拼接加载的需要放在静态目录下。如果配置完依旧缺字优先检查 release 包里是否包含 cmaps 目录有些精简版下载包不包含完整 cmaps字体解析就会失败。5.5 现象能选中文字但高亮位置和文字对不上文本层错位是接完 TextLayer 后最常见的体验问题。现象是鼠标选中文字时高亮背景落在错误的位置或者选中区域是乱跳的。原因有两个方向一是缩放时只重画了 canvas没有同步重算文本层二是文本层容器和 canvas 容器的坐标基准不一致。解决方法是让文本层容器和 canvas 容器严格重叠在同一个坐标系里。我通常把两个元素放在同一个父容器中父容器使用相对定位canvas 和 text-layer 都是绝对定位并且 text-layer 的尺寸、位置、缩放全部从同一个 viewport 对象推导。每次 scale 变化重绘 canvas 后必须立刻重新渲染文本层使用同一个 viewport。另一个细节是文本层的 CSS 里 line-height 必须设为 1默认的 line-height 会在行间产生额外偏移你看到的高亮框会比实际文字位置偏下半行。这一条如果出现不需要排查任何 pdf.js 内部逻辑问题基本都在 CSS 坐标一致性上。6. 大文件 PDF 的按需渲染优化可视区加载、取消渲染与最终质量权衡前面几章解决的是“能不能用”最后一章解决的是“大文件卡不卡”。几十页的 PDF 一次性全渲染出来再好的机器也扛不住所以在线查看工具的核心优化方向是只渲染用户当前能看到的页面。我常用的方案是滚动容器加可视区判断进入视口才渲染离开视口就 cleanup 并释放资源。function setupLazyRender(container, pages, renderPageFn) { let ticking false; container.addEventListener(scroll, () { if (ticking) return; ticking true; requestAnimationFrame(() { const rect container.getBoundingClientRect(); for (const page of pages) { const top page.offsetTop; const bottom top page.offsetHeight; if (bottom rect.top top rect.bottom) { // 进入可视区且未渲染才渲染 if (!page.rendered) { renderPageFn(page); } } else if (page.rendered) { // 离开可视区就释放资源 page.cleanup(); page.rendered false; } } ticking false; }); }); }这个函数有两点值得说明。一是用 requestAnimationFrame 节流避免 scroll 高频触发导致渲染调度混乱二是用 page.rendered 标记避免重复渲染同一页。这里的 page 结构里需要保存对应的 PDF 页面对象和 canvas这个结构可以在打开 PDF 后一次性建好也可以滚动到附近时再创建我倾向于后者因为前一种方式在几百页的大文件里光是创建空结构也会带来内存压力。在两三百页甚至更长的文档上我还会加一道渲染质量降级逻辑。初始渲染用较低的 scale如 0.8快速出图让用户先看到内容等滚动稳定之后再以完整 scale 重新渲染当前页。这个“先低清、后高清”的策略在桌面端体验提升不明显但在移动端和弱网环境下感知非常强烈代价是渲染次数变多所以必须配合 cancel 逻辑使用一旦用户继续滚动低清和高清渲染任务都取消只保留目标页的最终渲染。最后说一个我自己的习惯现在做在线查看工具只要遇到大文件标准配置一定是“按需渲染 离开视口 cleanup 渲染任务可取消”三件套。宁可让用户看到几毫秒的等待也不要让浏览器在后台默默越占越多。有一次在现场演示一个几百页的技术手册 PDF我因为忘了给上一页做 cleanup翻到一半页面开始掉帧最后靠着临时刷新才救回来。从那以后我就把内存释放写进了默认构造函数里而不是当成可选项。这些经验总结下来核心就是让渲染节奏跟着用户的眼睛走而不是把所有工作一股脑塞给浏览器。希望帮到你。本文还有配套的精品资源点击获取