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
相关产品推荐
相关产品推荐

