Optimize PDF loading with linearized documents
Linearized PDFs enable you to view a PDF while it downloads. For background on the format, refer to the linearized PDF blog post.
Linearized downloading requires only an HTTPS server. Document Engine streaming also supports editing during downloads and works with any PDF without preparation. For details, refer to the Document Engine streaming guide.
Comparison
The following table compares Document Engine streaming with linearized downloading.
| Document Engine streaming | Linearized downloading | |
|---|---|---|
| Fast initial page rendering | ||
| Only requires HTTPS server | ||
| Read-write while downloading | ||
| Only downloads the required data | ||
| Any PDF supported without preparation |
Enable linearized downloading
The viewer keeps PDFs read-only until the download finishes. You can view the content during the download, but you can’t add annotations or make other changes.
To enable linearized downloading, follow these steps:
- Linearize your PDF. For instructions, refer to the Document Engine linearization or .NET SDK linearization guide. Third-party tools sometimes call linearization “fast web loading.”
- Upload the linearized PDF to your HTTPS server.
- Load the PDF with
NutrientViewer.load.
The viewer enables the allowLinearizedLoading option by default. Use the following configuration:
NutrientViewer.load({ container: "#container", document: "https://example.com/path/to/linearized.pdf",});To disable linearized downloading, set allowLinearizedLoading: false.
Dynamic font loading and font substitution continue to work together while a linearized PDF downloads. They help avoid font fallbacks when the viewer first renders a partially downloaded PDF. For details, refer to the dynamic font loading guide and the font substitution guide.
How the viewer requests a document
The viewer requests the PDF data in this order:
- The viewer requests the first 64 KB, or the whole file if it’s smaller. This request has no time limit, so your server has no deadline to prepare the document.
- In Web SDK 1.23 and later, the viewer confirms the file’s length with small range requests. When the server exposes
Content-Range, one request asks for the last byte and the byte after it. Otherwise, the viewer sends two single-byte requests in parallel. A rangedHEADrequest provides a fallback. - The viewer sends up to two larger requests in parallel. These requests fetch the remaining data for the first page and the end of the file.
In Web SDK 1.25 and later, the viewer skips the first-page request when all three conditions apply:
- The viewer opens at the first page.
- The first page’s data exceeds 1 MB.
- The first page’s data exceeds half the file’s size.
In this case, the viewer fetches only the data that the first page draws.
Server requirements
The viewer uses HTTP range requests(opens in a new tab) (RFC 9110). For cross-origin PDFs, it also uses the cross-origin resource sharing (CORS) protocol(opens in a new tab) (Fetch Standard).
Configure your server to handle these requests:
- Answer range requests with
206 Partial Content(opens in a new tab). IncludeContent-Type: application/pdf. Return the full requested range, and don’t cap its size. - Answer a range that starts at or past the file’s end with
416 Range Not Satisfiable(opens in a new tab). When the server hidesContent-Range, this response confirms the file’s length if the viewer can’t use a rangedHEADrequest. For example, S3 presigned GET URLs rejectHEADrequests. A rangedHEADresponse with a matchingContent-Lengthalso confirms the length. - Expose
Content-Rangeto enable the viewer to confirm the length with one request. The requested range starts inside the file and extends past its end. Shorten the range to the file’s end(opens in a new tab). Return206. For example, a 1,000-byte file contains bytes zero through 999. A request withRange: bytes=900-1099returns the last 100 bytes andContent-Range: bytes 900-999/1000. - Stream the bytes instead of buffering the whole range. In Web SDK 1.23 and later, requests that check the length share a 15-second budget, including any
HEADfallback. Each larger request in step three has 15 seconds for its status and headers to arrive. If either request times out, the viewer uses smaller range requests without a console warning. Later chunk requests have no timeout.
The first request must succeed, including CORS checks for a cross-origin PDF. If that request returns an HTTP error or fails CORS checks, the viewer stops loading the document without a whole-file fallback. An HTTP error on a later chunk request also stops loading.
The following cases trigger a whole-file download instead of progressive loading:
| When | What happens |
|---|---|
The viewer can’t confirm the file’s length from the range requests or HEAD fallback (step two) | The viewer downloads the whole file before displaying the first page. The browser console shows a warning starting with Linearized loading disabled. The warning names the response that didn’t match. |
The content type isn’t application/pdf | The viewer downloads the whole file before displaying the first page. |
The viewer sends every request with credentials: 'same-origin', so it doesn’t send cookies to a cross-origin PDF server. Cookie-protected cross-origin PDFs fail to load. For header-based authentication, use the httpHeaders option. Include those headers in Access-Control-Allow-Headers.
For cross-origin PDFs, the browser enables the viewer to read only responses with CORS headers. Configure these headers:
- Send
Access-Control-Allow-Origin(opens in a new tab) on every response, including416. - Include
Rangeand any custom headers inAccess-Control-Allow-Headers. - Include
GETandHEADinAccess-Control-Allow-Methods. - Recommended — Expose
Content-RangeinAccess-Control-Expose-Headers(opens in a new tab). This enables one request to confirm the length. - Recommended — Set
Access-Control-Max-Age: 600to avoid a preflight before every range request.
Configure your server
Most web servers and frameworks handle range requests, including 416 responses, when serving a file or seekable stream. The following examples also add CORS headers for cross-origin PDFs. Replace https://your-app.example.com with your application’s origin.
Web servers
Both nginx and Apache answer range requests for static files, including 416 responses. Include Access-Control-Allow-Origin exactly once per response. Add CORS headers in either the web server or the application, not both.
Serve PDF files directly — Use add_header(opens in a new tab) without the always option. For generated 416 responses, nginx keeps these headers. The always option would add them twice:
location ~ \.pdf$ { if ($request_method = OPTIONS) { add_header Access-Control-Allow-Origin "https://your-app.example.com"; add_header Access-Control-Allow-Methods "GET, HEAD"; add_header Access-Control-Allow-Headers "Range"; add_header Access-Control-Max-Age 600; return 204; }
add_header Access-Control-Allow-Origin "https://your-app.example.com"; add_header Access-Control-Expose-Headers "Content-Range";}Proxy requests to an application — Use add_header with the always option. This adds the headers to the application’s 416 responses. If the application already sends CORS headers, don’t add them in nginx:
location /documents/ { if ($request_method = OPTIONS) { add_header Access-Control-Allow-Origin "https://your-app.example.com"; add_header Access-Control-Allow-Methods "GET, HEAD"; add_header Access-Control-Allow-Headers "Range"; add_header Access-Control-Max-Age 600; return 204; }
proxy_pass http://your-backend; add_header Access-Control-Allow-Origin "https://your-app.example.com" always; add_header Access-Control-Expose-Headers "Content-Range" always;}Use Header always set, which requires mod_headers. The Header set directive applies only to successful responses, so it omits CORS headers from 416 responses:
<FilesMatch "\.pdf$"> Header always set Access-Control-Allow-Origin "https://your-app.example.com" Header always set Access-Control-Allow-Methods "GET, HEAD" Header always set Access-Control-Allow-Headers "Range" Header always set Access-Control-Expose-Headers "Content-Range" Header always set Access-Control-Max-Age "600"</FilesMatch>Application frameworks
If your application serves the document, use the framework’s range handling. If your endpoint generates the document, implement range handling as described in the server requirements section.
In Express, express.static and res.sendFile answer range requests, including 416 responses. Add CORS headers with the cors middleware(opens in a new tab):
const express = require("express");const cors = require("cors");
const app = express();
app.use( "/documents", cors({ origin: "https://your-app.example.com", methods: ["GET", "HEAD"], allowedHeaders: ["Range"], exposedHeaders: ["Content-Range"], maxAge: 600, }), express.static("/path/to/documents"),);Enable range processing when returning the document. Map both GET and HEAD. ASP.NET Core then returns 206 and 416 responses. The CORS middleware adds its headers to every response:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options => options.AddDefaultPolicy(policy => policy .WithOrigins("https://your-app.example.com") .WithMethods("GET", "HEAD") .WithHeaders("Range") .WithExposedHeaders("Content-Range") .SetPreflightMaxAge(TimeSpan.FromMinutes(10))));
var app = builder.Build();app.UseCors();
app.MapMethods("/documents/{id:int}", new[] { "GET", "HEAD" }, IResult (int id) =>{ var path = Path.Combine("/path/to/documents", $"{id}.pdf"); if (!File.Exists(path)) { return Results.NotFound(); }
Stream pdf = File.OpenRead(path); return Results.File(pdf, "application/pdf", enableRangeProcessing: true);});
app.Run();Check server support
Use curl to check that the server answers the viewer’s requests. The following commands send GET requests with a Range header, like the viewer does.
A HEAD request isn’t a reliable test because some servers reject it even when range requests work. For example, S3 presigned URLs support specific HTTP methods. A URL signed for GET requests rejects HEAD requests.
First, check that the server accepts range requests. Expect a 206 response:
curl -r 0-1 https://example.com/path/to/linearized.pdf -o /dev/null -w "%{http_code}\n"Next, check the requests that confirm the file’s length. Replace <size> with the file’s size in bytes. Replace <size-1> with that size minus one:
# Expect 206 with Content-Range: bytes <size-1>-<size-1>/<size># and Content-Type: application/pdf when Content-Range is exposed.curl -s -o /dev/null -D - -r "<size-1>-<size>" https://example.com/path/to/linearized.pdf# When Content-Range is hidden, check the two single-byte requests instead.# Expect 206 with Content-Type: application/pdf.curl -s -o /dev/null -D - -r "<size-1>-<size-1>" https://example.com/path/to/linearized.pdf# Expect 416.curl -s -o /dev/null -D - -r "<size>-<size>" https://example.com/path/to/linearized.pdfFor a cross-origin PDF, add -H "Origin: https://your-app.example.com" to each command. Check that every response includes Access-Control-Allow-Origin.
Additional information
Keep these limits and storage settings in mind:
- Linearized downloading enables viewing during downloads, not editing. You can’t modify the PDF until the download finishes.
- Linearized downloading applies only to PDFs that the viewer downloads from an external web server.
- Nutrient Web SDK manages temporary storage for downloads in the browser. For larger eligible PDFs, it can use the browser’s origin private file system (OPFS)(opens in a new tab). This reduces peak browser RAM usage during downloads. To disable this behavior, set
tempStorage: "memory". For details, refer to the temporary storage guide.