Splitting a PDF by file size is one of the most practical but technically tricky operations. Unlike splitting by page count (simple math) or bookmarks (tree traversal), size-based splitting requires estimating and controlling the output size of each chunk — and PDFs don't have a simple "size per page" property.
Here's how to build a browser-based PDF splitter that respects file size constraints.
The challenge
PDFs are notoriously unpredictable in terms of size. Two PDFs with the same number of pages can differ by 10x in file size depending on:
- Image resolution and compression
- Font embedding
- Color space (RGB vs. CMYK)
- Content complexity (vector graphics vs. scanned images)
This means you can't calculate split points with simple arithmetic. You need to estimate, test, and adjust.
The stack
- Vue 3 with Composition API
- pdf-lib for PDF manipulation
- Vite for bundling
The core implementation
The approach is greedy accumulation with size estimation:
<script setup lang="ts">
import { ref } from 'vue'
import { PDFDocument } from 'pdf-lib'
const file = ref<File | null>(null)
const targetSizeMB = ref<number>(10)
const compression = ref<'none' | 'low' | 'high'>('low')
const splitting = ref(false)
const progress = ref(0)
const progressTotal = ref(0)
const results = ref<Record<string, Uint8Array>>({})
async function splitBySize() {
if (!file.value) return
splitting.value = true
const arrayBuffer = await file.value.arrayBuffer()
const pdf = await PDFDocument.load(arrayBuffer)
const totalPages = pdf.getPageCount()
const targetBytes = targetSizeMB.value * 1024 * 1024
const outputFiles: Array<{ name: string; data: Uint8Array }> = []
let currentPdf = await PDFDocument.create()
let currentSize = 0
let pageNum = 0
for (let i = 0; i < totalPages; i++) {
progressTotal.value = totalPages
progress.value = i + 1
// Try adding this page
try {
const [copiedPage] = await currentPdf.copyPages(pdf, [i])
currentPdf.addPage(copiedPage)
// Estimate size by saving
const estimated = await currentPdf.save({
compress: compression.value !== 'none',
updateMetadata: false,
})
currentSize = estimated.byteLength
// If we'd exceed target and this isn't the last page, close current file
if (currentSize >= targetBytes && i < totalPages - 1) {
outputFiles.push({
name: `part-${pageNum + 1}.pdf`,
data: await currentPdf.save({ compress: true }),
})
pageNum++
currentPdf = await PDFDocument.create()
currentSize = 0
// Retry adding this page to the new file
i-- // Re-process this page in the new file
continue
}
} catch {
// Page couldn't be copied — skip it
console.warn(`Page ${i + 1} skipped due to error`)
}
}
// Don't forget the last file
if (currentSize > 0) {
outputFiles.push({
name: `part-${pageNum + 1}.pdf`,
data: await currentPdf.save({ compress: true }),
})
}
results.value = Object.fromEntries(
outputFiles.map(f => [f.name, f.data])
)
splitting.value = false
}
</script>
Key implementation details
1. Why incremental save for estimation?
PDF.js and pdf-lib don't provide a reliable "what will this page contribute to file size?" API. The most accurate estimation is to actually save the PDF and check the byte length. This is done incrementally:
const estimated = await currentPdf.save()
currentSize = estimated.byteLength
This is slow for large documents (each save is O(pages²) in the worst case), but it's accurate. For better performance with very large PDFs, you could use a sampling approach: save every 5th page to estimate, then adjust.
2. The retry logic
When a page would push the current file over the size limit, we close the current file and re-process the same page in a new file:
if (currentSize >= targetBytes && i < totalPages - 1) {
outputFiles.push(...)
currentPdf = await PDFDocument.create()
currentSize = 0
i-- // Re-process this page
continue
}
This ensures no page is lost — even if it doesn't fit in the target size on its own, it goes into its own file.
3. Compression settings
pdf-lib supports compression when saving:
await pdf.save({ compress: true }) // Compress streams (JPEG for images)
await pdf.save({ compress: false }) // Keep original compression
Higher compression reduces file size but may reduce image quality. Let the user choose:
- None: No additional compression (fastest, largest files)
- Low: Mild compression (good balance)
- High: Aggressive compression (smallest files, lower quality)
4. Progress indication
Since size estimation requires incremental saves, large PDFs can take time. Show progress:
progress.value = i + 1 // Current page being processed
progressTotal.value = totalPages
5. Edge case: single page exceeds target
If even a single page exceeds the target size (e.g., a full-page high-resolution scanned image), the algorithm will still create a file for it. The user should be warned that some files may exceed the target size.
Summary
Building a browser-based "split by file size" tool involves:
- Loading the PDF and setting a target size
- Greedily accumulating pages into output files
- Estimating file size via incremental saves
- Closing files and starting new ones when targets are exceeded
- Packaging all outputs as a ZIP
The result: a large PDF split into chunks that respect your size constraints. Try it at en.sotool.top/split-by-size.