PDFJS优化:PDF文件分片加载

作者:linkyang
标签:pdf
发布时间:2025年05月06日 22:58:45
更新时间:2025年05月06日 22:58:45

一、为什么需要使用分片加载?

  • 提高加载速度:将 PDF 文件分成多个小片后,浏览器可以同时加载多个片段,利用多线程或并行加载的机制,加快整体的加载过程。尤其是对于较大的 PDF 文件,用户无需等待整个文件完全下载后才开始查看,能更快地看到文件的部分内容,节省等待时间。
  • 改善用户体验:分片加载允许用户在文件尚未完全加载时就开始浏览已加载的部分,对于长文档或包含大量图片、图表的 PDF,用户可以立即开始阅读文本内容,而不必面对长时间的空白页面或加载进度条,提升阅读体验。
  • 适应不同网络环境:在网络不稳定或带宽有限的情况下,分片加载更为可靠。如果某个片段加载失败,可以单独重新加载该片段,而不是重新下载整个文件,降低了因网络问题导致加载失败的风险,提高了文件加载的成功率。
  • 节省内存:一次性加载整个大型 PDF 文件可能会占用大量内存,导致浏览器或应用程序运行缓慢甚至卡顿。分片加载则是按需加载,每次只在内存中存储当前需要显示的片段,减轻了内存压力,使系统能够更高效地运行,尤其对于移动设备或性能有限的计算机,这一点更为重要。

二、PDFJS分片相关核心配置参数

配置项类型默认值功能说明
rangeChunkSizenumber65536定义分片请求的字节粒度
disableStreambooleanfalse禁用TCP流式传输
disableAutoFetchbooleanfalse关闭PDF.js的预加载机制,仅加载可视区域内容

三、分片加载触发机制

  1. 首次请求协商 当PDF.js发起初始请求时,服务端需在响应头中声明:
    http
    Content-Length: 1234567       // 完整文件字节数
    Accept-Ranges: bytes          // 声明支持字节范围请求
    

    此举将激活PDF.js的分片请求模式
  2. 动态范围请求 后续请求将携带Range头实现精准加载:
    http
    GET /document.pdf
    Range: bytes=0-1048575        // 请求前1MB数据
    

三、开发注意事项

  1. 避免一次性渲染所有pdf页面
    javascript
    // 错误做法:触发全部分片请求
    Array(pdf.numPages).forEach(renderPage);
    
    // 正确做法:基于IntersectionObserver动态加载
    observer.observe(pageContainer);
    
  2. 服务端必须实现的功能
    • 正确处理HEAD请求返回Content-LengthAccept-Ranges
    • 支持Range请求的合法性校验(HTTP 416状态码处理)

四、代码案例

前端相关配置

js
const loadingTask = pdfjsLib.getDocument({
    url: '',//文档请求地址
    httpHeaders: {}, //自定义请求头,例如可以携带token
    disableAutoFetch: true, //是否禁用预加载
    disableStream: true,//是否禁用流
    rangeChunkSize: 1024 * 1024 * 2,//文件分片大小
    cMapUrl: '/cmaps/',
    cMapPacked: true,
  })

后端接口实现

我这里后端使用的是eggjs,其他框架和编程语言按照这个思路去实现就行。

js
  // PDF分片预览
  async previewFile(fileId) {
    const { ctx } = this
    //从数据库中查询
    const file = await ctx.model.FileTable.findOne({
      where: {
        fileId,
      },
    })
    if (!file) {
      throw new Error('文件不存在')
    }
    const filePath = file.originalPath ? path.join('app', file.originalPath) : path.join('app', file.filePath)

    // 检查文件是否存在
    if (!fs.existsSync(filePath)) {
      ctx.status = 404
      ctx.body = { error: '文件路径不存在' }
      return
    }

    const stat = fs.statSync(filePath) // 获取文件信息
    const total = stat.size // 文件总大小
    const range = ctx.get('range') // 获取请求头中的 Range

    if (range) {
      // 解析 Range 请求头
      const parts = range.replace(/bytes=/, '').split('-')
      const start = parseInt(parts[0], 10)
      const end = parts[1] ? parseInt(parts[1], 10) : total - 1

      // 检查范围合法性
      if (start >= total || end >= total) {
        ctx.status = 416 // Range Not Satisfiable
        ctx.set('Content-Range', `bytes */${total}`)
        return
      }

      // 设置响应头
      ctx.status = 206 //必须设置206,代表返回的是部分资源
      ctx.set('Content-Range', `bytes ${start}-${end}/${total}`)
      ctx.set('Accept-Ranges', 'bytes')
      ctx.set('Content-Length', end - start + 1)
      ctx.set('Content-Type', 'application/pdf')

      // 返回文件段
      ctx.body = fs.createReadStream(filePath, { start, end })
    } else {
      // 设置响应头,告诉前端后台支持文件分片
      ctx.set('Content-Length', total) //必须设置
      ctx.set('Accept-Ranges', 'bytes')//必须设置
      ctx.set('Content-Type', 'application/pdf')
      ctx.set('Content-Range', `bytes 0-${total - 1}/${total}`) // 返回文件总大小
      ctx.set('Access-Control-Expose-Headers', 'Accept-Ranges,Content-Range')
      ctx.body = fs.createReadStream(filePath) // 返回整个文件流
    }
  }

五、关于分片后还是请求了大量的数据

在官方仓库的Issues中有人提出了这个问题,这个问题官方在这个这个更新补丁中也提到了。有些pdf文件的页数有可能是不正确的,这样会导致pdf在解析的时候异常中断并导致浏览器异常挂起。所以官方更新这个补丁在打开pdf的时候检查了最后一页来确保pdf的页数是正确的,所以需要加载更多的数据。也会减慢pdf加载的速度。

如果可以保证PDF文件的完整性可以使用2.3.200或以下的版本,就是没有包含这个补丁的版本,实际测试的确速度更快,也可以使用qpdf对pdf文件进行线性化优化,也可以优化加载速度。

登录后可查看并参与评论

Gitee 登录

目录导航

一、为什么需要使用分片加载?
二、PDFJS分片相关核心配置参数
三、分片加载触发机制
三、开发注意事项
四、代码案例
前端相关配置
后端接口实现
五、关于分片后还是请求了大量的数据