"Make this PDF under 5 MB" sounds like a setting.
It is not.
For a browser-based PDF tool, that request is a constrained search problem with a user-facing promise attached to it:
output.size <= chosen limit
while preserving as much readable page appearance as possible
without making the interface feel stuck
The easy implementation is a quality slider. The hard part is delivering a result that is actually below the chosen limit, explaining the trade-off honestly, and not turning every adjustment into another full PDF render.
This is how I approached target-size PDF compression in a browser-only tool.
The request is deceptively simple
Users do not normally ask for "JPEG quality 0.64" or "render at 1.13x." They ask for something concrete:
- "This portal only accepts PDFs below 500 KB."
- "My email attachment has to stay below 5 MB."
- "I need to upload this form, but the file limit is 1 MB."
That means the product contract is different from ordinary compression. A result that looks good but is 501 KB for a 500 KB upload limit is still a failure.
At the same time, a result that is technically 100 KB but destroys legibility is not a useful success. The goal is not to create the smallest PDF. The goal is to find the largest acceptable output that still obeys the user's limit.
The first bad idea: re-render the PDF on every attempt
The naive loop looks like this:
- Render every PDF page.
- Encode each page as JPEG at one quality level.
- Build a new PDF.
- Check its byte size.
- Repeat from step 1 if it is too large.
It works, but it repeats the most expensive part of the job. A multi-page PDF can require several full page renders before the search settles near the target. That wastes CPU time, reallocates large canvases, and makes the UI feel increasingly slow as documents get longer.
The more useful question is not "Which quality preset should I use?" It is:
How can I reuse the expensive work while searching two variables: render scale and JPEG quality?
Turn repeated rendering into a bounded search
The workflow starts by preparing each page once at a bounded source resolution. The browser renders the PDF page to a canvas, encodes a reusable source JPEG, records the original page dimensions, and releases the canvas. There is also a memory budget, because a browser tool should not try to hold an unlimited number of huge page images in memory just to chase a smaller result.
After that preparation stage, each candidate is described by two values:
{ scale, quality }
scale changes the number of pixels available to the page. quality changes JPEG encoding fidelity. Both affect the final byte size, but neither maps perfectly to it. A mostly-text page, a scanned page, and a photo-heavy brochure respond very differently.
The search begins at the highest safe settings. For every candidate, the compressor records one of two things:
- the closest candidate that is still too large; or
- the largest candidate already at or below the requested limit.
Once there is one candidate on each side of the limit, the next attempt can refine between them instead of blindly dropping quality. The implementation uses logarithmic interpolation for that refinement because file sizes do not behave like a neat linear slider.
Here is the essential shape of the search:
let bestFit = null;
let tooLarge = null;
let settings = highestSafeSettings;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const blob = await buildCandidate(settings);
if (blob.size <= targetBytes) {
bestFit = keepTheLargerOf(bestFit, { blob, settings });
settings = refineBetween(tooLarge, bestFit, targetBytes);
} else {
tooLarge = keepTheSmallerOf(tooLarge, { blob, settings });
settings = lowerScaleAndQuality(settings, blob.size, targetBytes);
}
}
return bestFit;
There are two important details here:
- The output is measured after the PDF has actually been encoded. Estimating size from dimensions or JPEG quality is not enough.
- The search has strict lower bounds and a fixed attempt limit. If the minimum readable settings cannot meet the request, the tool has to say so.
A Worker helps, but it is not magic
Preparing source pages still requires browser PDF rendering. The repeated candidate assembly is where the work becomes especially wasteful, so that stage runs in a Web Worker when the browser supports it.
The Worker receives the prepared page images and builds candidate PDFs away from the interaction path. The main page can keep showing progress while candidates are encoded and measured. If the Worker cannot start, or if the prepared sources would exceed the safe memory budget, the tool falls back to a compatible in-page path instead of failing the conversion entirely.
That distinction matters. A Worker is not a claim that every byte of PDF work suddenly becomes free or GPU-accelerated. It is a way to avoid repeatedly tying up the interface during the part of the search that can safely happen elsewhere.
The honesty requirement: visual preservation is not semantic preservation
There is a product trade-off that must be visible before the user clicks the button.
For strict target-size compression, the tool rebuilds pages as optimized JPEG images inside a new PDF. That preserves the visible page appearance, but it does not preserve:
- selectable and searchable text;
- links;
- form fields;
- bookmarks; or
- accessibility tags.
It would be easy to hide that limitation under a generic "high compression" label. That would also be misleading. A user sending a scanned receipt may be perfectly happy with an image-based result. A user submitting an accessible form or a searchable contract probably should keep the original.
So the interface states the trade-off in the target-size mode itself, rather than discovering it only after download.
Verification is the feature, not the progress bar
The result screen is deliberately simple: show the original size, the final size, the percentage reduction, and whether the result meets the exact cap.
In one local test with a three-page image-heavy PDF, the input was 1.09 MB. The chosen maximum was 500 KB. The completed output was 473.0 KB, or 58% smaller. The download became available only after the final bytes were verified against the limit.
The opposite outcome is equally important. If the compressor reaches its declared minimum settings and still cannot satisfy the limit, it should not show a green success state or hand the user a file that will fail their upload. "Cannot meet this limit safely" is a valid product result.
What I learned
The interesting part of a target-size compressor is not the slider. It is the decision to treat the chosen size as a contract:
- prepare expensive source data once;
- search bounded combinations of scale and quality;
- keep the best valid result rather than the first valid result;
- isolate repeated assembly work when the browser can support it;
- clearly disclose what a rasterized result loses; and
- refuse to label an over-limit file as successful.
That pattern applies beyond PDFs. Any browser feature that promises an exact output size, duration, or quota is usually not one knob. It is a constrained optimization problem with product decisions around honesty, fallbacks, and failure states.
You can try the browser-only implementation here: Compress PDF to a Target Size.



Top comments (0)