You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js项目中react-pdf在iPhone13及以下机型显示PDF崩溃求助

旧iOS Safari下react-pdf多PDF渲染崩溃问题解决方案

针对iPhone 13及更早机型(iOS 15/16 Safari)上react-pdf渲染多个PDF时页面崩溃的问题,核心原因是旧设备内存管控严格,一次性渲染大量PDF页面或多Worker同时运行会快速耗尽内存触发崩溃。以下是针对性优化方案:

1. 页面懒加载:避免一次性渲染所有PDF页

当前代码会一次性渲染PDF的所有页面,这是内存消耗的主要来源。改用可视区域懒加载,只渲染用户能看到及即将看到的页面,其余用占位符替代:

修改PDFCard组件:

import React, { useState, useEffect, useRef } from "react";
import { Document, Page } from "react-pdf";

// 保留已有的Promise.withResolvers polyfill
if (typeof Promise.withResolvers === "undefined") {
  if (window) {
    // @ts-expect-error
    window.Promise.withResolvers = function () {
      let resolve, reject;
      const promise = new Promise((res, rej) => {
        resolve = res;
        reject = rej;
      });
      return { promise, resolve, reject };
    };
  }
}

// 改用兼容旧iOS的pdfjs worker(建议配合降级后的pdfjs版本)
import { pdfjs } from "react-pdf";
pdfjs.GlobalWorkerOptions.workerSrc = "https://unpkg.com/pdfjs-dist@3.11.174/legacy/build/pdf.worker.min.js";

const PDFCard = ({ pdf }: { pdf: string }) => {
  const [numPages, setNumPages] = useState(0);
  const [pageWidth, setPageWidth] = useState(500); // 补上缺失的pageWidth状态
  const [visiblePages, setVisiblePages] = useState<number[]>([1]); // 初始加载第1页
  const pageRefs = useRef<Record<number, HTMLElement | null>>({});
  const documentRef = useRef<any>(null);

  const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
    setNumPages(numPages);
  };

  // 监听页面可视区域,动态加载/卸载页面
  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          const pageNum = parseInt(entry.target.dataset.pageNumber || "0");
          if (!pageNum) return;

          if (entry.isIntersecting) {
            // 加载当前页及前后各1页,保证滚动流畅
            const newVisible = new Set(visiblePages);
            newVisible.add(pageNum);
            if (pageNum > 1) newVisible.add(pageNum - 1);
            if (pageNum < numPages) newVisible.add(pageNum + 1);
            setVisiblePages(Array.from(newVisible));
          } else {
            // 移除超出可视范围2页外的页面,释放内存
            setVisiblePages(prev => prev.filter(p => Math.abs(p - pageNum) <= 2));
          }
        });
      },
      { rootMargin: "200px" } // 提前200px预加载
    );

    Object.values(pageRefs.current).forEach(ref => ref && observer.observe(ref));
    return () => observer.disconnect();
  }, [numPages, visiblePages]);

  // 窗口resize监听(保留原有逻辑)
  useEffect(() => {
    const updatePageWidth = () => {
      const containerWidth = document.querySelector<HTMLElement>(".pdf-container")?.offsetWidth || 500;
      setPageWidth(containerWidth);
    };
    updatePageWidth();
    window.addEventListener("resize", updatePageWidth);
    return () => window.removeEventListener("resize", updatePageWidth);
  }, []);

  // 组件卸载时销毁PDF实例,释放内存
  useEffect(() => {
    return () => {
      if (documentRef.current) {
        documentRef.current.destroy();
      }
    };
  }, []);

  if (!pdf) return null;

  return (
    <div className="pdf-container">
      <Document
        ref={documentRef}
        file={pdf}
        onLoadSuccess={onDocumentLoadSuccess}
      >
        {Array.from({ length: numPages }, (_, index) => {
          const pageNum = index + 1;
          return visiblePages.includes(pageNum) ? (
            <Page
              key={`page_${pageNum}`}
              pageNumber={pageNum}
              renderAnnotationLayer={false}
              renderTextLayer={false}
              width={pageWidth}
              ref={el => pageRefs.current[pageNum] = el}
              data-page-number={pageNum}
            />
          ) : (
            // 用占位符保持页面结构,减少DOM节点波动
            <div
              key={`placeholder_${pageNum}`}
              style={{
                width: pageWidth,
                height: pageWidth * 1.414, // 近似A4纸比例
                backgroundColor: "#f5f5f5"
              }}
            />
          );
        })}
      </Document>
    </div>
  );
};

2. 限制同时渲染的PDF数量:按需加载多PDF组件

如果页面存在多个PDFCard,不要一次性全部渲染,只加载可视区域内的PDF,离开可视区域后卸载释放内存:

修改父组件:

import dynamic from "next/dynamic";
import React, { useState, useEffect, useRef } from "react";
const PDFCard = dynamic(() => import("../PDFCard"), { ssr: false });

const PDFList = ({ pdfUrls }: { pdfUrls: string[] }) => {
  const [visiblePdfIndexes, setVisiblePdfIndexes] = useState<number[]>([]);
  const pdfCardRefs = useRef<Record<number, HTMLElement | null>>({});

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          const index = parseInt(entry.target.dataset.pdfIndex || "0");
          if (entry.isIntersecting) {
            setVisiblePdfIndexes(prev => [...new Set([...prev, index])]);
          } else {
            // 离开可视区域后卸载PDF,释放内存
            setVisiblePdfIndexes(prev => prev.filter(i => i !== index));
          }
        });
      },
      { rootMargin: "300px" }
    );

    Object.values(pdfCardRefs.current).forEach(ref => ref && observer.observe(ref));
    return () => observer.disconnect();
  }, []);

  return (
    <div className="pdf-list">
      {pdfUrls.map((url, index) => (
        <div
          key={`pdf_container_${index}`}
          ref={el => pdfCardRefs.current[index] = el}
          data-pdf-index={index}
          style={{ marginBottom: "20px" }}
        >
          {visiblePdfIndexes.includes(index) ? (
            <PDFCard pdf={url} />
          ) : (
            <div style={{ padding: "40px", textAlign: "center", backgroundColor: "#f5f5f5" }}>
              即将加载PDF
            </div>
          )}
        </div>
      ))}
    </div>
  );
};

3. 降级pdfjs-dist版本适配旧iOS

react-pdf@9.1.1依赖的高版本pdfjs-dist对旧iOS Safari兼容性不佳,建议降级到更稳定的3.x版本:

npm install pdfjs-dist@3.11.174

同时修改worker配置为.js格式(旧iOS对ES模块支持有限):

pdfjs.GlobalWorkerOptions.workerSrc = "https://unpkg.com/pdfjs-dist@3.11.174/legacy/build/pdf.worker.min.js";

4. 极端情况:禁用Web Worker

如果Web Worker在旧iOS下频繁触发异常,可尝试禁用Worker改用主线程渲染(会牺牲部分性能,仅作为最后手段):

pdfjs.GlobalWorkerOptions.workerSrc = undefined;

验证方法

用Mac连接iPhone,打开Safari开发者工具的内存面板,监控内存占用变化,确认优化后内存峰值明显下降,崩溃情况消失。

内容的提问来源于stack exchange,提问作者FD3

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 09:49:59