PAYATHON 2026

为什么 PuppeteerSharp 首次调用 GoToAsync 生成 PDF 更慢?

数据小许

首次导航的额外耗时需要继续拆分定位

这组日志显示首次导航耗时较长,符合存在冷启动开销的表现,但还不能确定具体原因。GoToAsync 首次耗时 3033 毫秒,后续为 252~294 毫秒,相差约 2.7~2.8 秒。仅凭这些数字,无法判断时间花在 HTML 服务、资源加载、页面脚本,还是浏览器内部初始化上。

using 会释放对应对象持有的资源,但不会清空整个系统的缓存,也不会重启提供 HTML 的服务。因此,即使每次请求都重新启动 Chrome,后续访问仍可能更快。

GoToAsync 的计时包含哪些工作

GoToAsync 的耗时包括下载 HTML,也受导航等待条件的影响。这段时间可能涉及服务器响应、页面依赖资源加载,以及影响加载进度的脚本执行。

当前代码没有显式指定导航等待条件。排查时应核对所用 PuppeteerSharp 版本的默认行为,并确认该条件是否符合页面实际的打印要求。

可能导致首次访问变慢的因素包括:

  • 提供 heavy.html 或其依赖资源的服务可能存在延迟初始化,使第一次访问需要额外时间。如果页面只是静态文件,则应重点检查实际文件读取和响应耗时。
  • 首次读取 Chrome 文件、页面资源或字体后,操作系统可能保留相关文件数据,后续新进程也可能受益。
  • 浏览器及渲染环境的某些工作可能在首次导航或首次使用相关资源时发生。是否存在这些工作、占用多少时间,需要进一步测量。
  • 本地 HTML 也可能引用外部脚本、样式、字体或接口。没有 heavy.html 的内容和网络记录,不能排除这些因素。

这些都是排查方向,不能直接认定某一项解释了全部额外耗时。

using 为什么没有消除差异

原代码每次调用接口都会执行 Puppeteer.LaunchAsync,随后释放页面和浏览器。代码没有展示跨请求复用同一个浏览器实例的逻辑。

关闭浏览器不会清除所有其他状态。提供 HTML 的服务可能一直运行,操作系统文件缓存也可能继续存在。

也不能据此认定后续请求一定复用了 Chrome 的 HTTP 缓存。跨浏览器进程是否存在可复用的浏览器缓存,需要结合实际用户数据目录及启动配置确认;给出的代码不足以证明这一点。

按层次排查首次请求

先检查 https://localhost/heavy.html 及其依赖资源在服务端的访问日志,记录首次和后续请求的处理时间。如果首次服务器响应已经明显更慢,就先处理该服务的初始化或资源读取问题。

接着,在 Chrome 开发者工具中观察该页面的网络瀑布图和 Performance 记录,重点查看:

  1. 主文档响应是否慢。
  2. 是否有字体、脚本、样式或接口在首次访问时耗时较长。
  3. 是否存在占用主线程较久的脚本。
  4. 页面达到打印就绪状态的时间是否与导航完成时间一致。

普通浏览器的记录有助于识别页面问题,但它与 PuppeteerSharp 启动的浏览器配置可能不同,不能直接把两者的绝对耗时等同起来。

还可以做一个对照实验:在同一个浏览器、同一个页面中连续导航两次。下面的代码用于单独观察导航,不包含 PDF 生成:

using (var browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
    ExecutablePath = "C:\\ChromeHeadless\\chrome-headless-shell.exe",
    Headless = true
}))
{
    using (var page = await browser.NewPageAsync())
    {
        for (int attempt = 1; attempt <= 2; attempt++)
        {
            var stopwatch = Stopwatch.StartNew();

            await page.GoToAsync("https://localhost/heavy.html");

            stopwatch.Stop();
            _logger.LogInformation(
                "Navigation {attempt}: {elapsed} ms",
                attempt,
                stopwatch.ElapsedMilliseconds);
        }
    }
}

这个实验与原代码每次启动新浏览器的方式不同,可以观察浏览器存活期间的重复导航表现,但不能单独证明差异由缓存导致。

进一步测试时,每次只改变一个条件,例如只重启 PDF API、只重启 HTML 服务,或只重新启动浏览器。如果两个服务运行在同一进程中,需要先区分它们的影响。每组重复多次,并记录首次请求和后续请求,避免根据一次测量下结论。

可以怎样改善性能

对于持续提供 PDF 生成的服务,可以考虑由应用管理一个长期运行的浏览器实例,每次任务创建独立页面,完成后关闭页面。这样能够减少重复启动浏览器的开销,也便于保留进程内的初始化状态。

实现时需要管理浏览器的启动与退出、异常后的重建,以及并发数量。涉及不同用户的数据隔离时,还应根据所用版本选择合适的独立浏览器上下文。不要让多个请求同时操作同一个页面。

不过,日志中的浏览器启动耗时只有 114~316 毫秒。复用浏览器有优化价值,但不能保证消除首次导航多出的约 2.8 秒。

如果进一步验证表明额外耗时来自首次初始化,可以按实际生成路径预热:在服务接收业务请求之前执行一次代表性的页面加载,必要时执行一次 PDF 生成。预热会把部分等待提前到启动阶段,增加启动耗时;预热产生的浏览器进程内状态,也只有在该进程继续存活时才能继续利用。

如果确认默认导航等待条件等待了与打印无关的资源,可以根据页面状态调整等待策略,但必须保留对必要字体、图片和业务数据的就绪检查。过早生成 PDF 可能导致内容缺失或排版变化,不能只以 GoToAsync 数字下降作为优化成功的依据。

PDF 生成阶段也值得单独检查。后续请求中,PdfStreamAsync 始终耗时约 2.2 秒,明显高于导航耗时。即使解决首次导航问题,这部分等待仍然存在。如果目标是降低所有请求的延迟,需要继续检查页面规模、图片、字体、分页及打印样式;缺少 HTML 内容时,无法给出更具体的瓶颈结论。

这四个计时区间没有覆盖接口返回文件到客户端的全过程。比较用户实际等待时间时,还应记录端到端请求耗时,并保持页面内容、启动方式和测试环境一致。

备注:内容仅供参考。