Automated document generation pipelines often rely on headless browsers to compile HTML and CSS layouts into high-fidelity PDFs. The modern industry standard is Playwright. However, Playwright introduces a significant operational constraint: the strict requirement to install and maintain specialized browser binaries within the local runtime.

In highly restricted system environments—such as sandboxed user spaces, air-gapped CI/CD runners, or low-priority background threads throttled by CPU resource limits (e.g., executing commands via nice -n 19)—running the installer script npx playwright install can hang indefinitely at 0% or throw system locks.

To avoid bringing down critical document-generation pipelines, the code must implement a resilient architectural fallback. By leveraging dynamic imports and trapping runtime launch failures, we can seamlessly reroute execution to the native, system-installed Google Chrome CLI.


1. Isolating Playwright with Dynamic Imports

A major design flaw in standard Node.js automation scripts is using static import statements at the top of the file:

// If playwright is not fully installed, the entire module fails to parse on startup
import playwright from 'playwright';

If the package is not installed or contains broken binary links, the entire script crashes before executing any logical checks. To prevent this, wrap the module resolution in a dynamic import() expression:

let playwright;
try {
  playwright = await import('playwright');
} catch (err) {
  console.warn("⚠️ Playwright package is missing from node_modules. Falling back to system Chrome CLI.");
}

2. Catching Launch Failures and Directory Locks

Even if the package is present in package.json, calling chromium.launch() can fail if the driver binaries are corrupt, missing, or blocked by a parallel process holding directory write locks (__dirlock).

Wrap the browser constructor in a robust try/catch block:

let browser;
let useFallback = false;

if (playwright) {
  try {
    browser = await playwright.chromium.launch({
      headless: true,
      args: ['--no-sandbox', '--disable-setuid-sandbox']
    });
  } catch (launchError) {
    console.warn("⚠️ Playwright launch failed (missing driver/locks). Activating native browser fallback.");
    useFallback = true;
  }
} else {
  useFallback = true;
}

3. Implementing the System Chrome Fallback

If the execution flips the useFallback trigger, we bypass the Playwright library entirely. Every major modern OS (Linux, macOS, Windows) has a native browser binary accessible via the shell CLI.

We can spawn a child process to run Google Chrome in headless print-to-pdf mode. The shell command is clean and doesn’t require extra library overhead:

import { execSync } from 'child_process';

function renderPdfViaCLI(inputHtmlPath, outputPdfPath) {
  // Common executable aliases for Chrome/Chromium across distributions
  const binaries = ['google-chrome-stable', 'google-chrome', 'chromium-browser', 'chromium'];
  let success = false;

  for (const binary of binaries) {
    try {
      const command = `${binary} --headless --disable-gpu --no-sandbox --print-to-pdf="${outputPdfPath}" "${inputHtmlPath}"`;
      execSync(command, { stdio: 'ignore' });
      success = true;
      console.log(`✅ PDF successfully generated using native shell: ${binary}`);
      break;
    } catch (e) {
      // Continue to next binary option
    }
  }

  if (!success) {
    throw new Error("Fatal: No local Chromium or Google Chrome binaries found on PATH.");
  }
}

Conclusion: Designing for System Robustness

Relying on external packaging frameworks to download hundreds of megabytes of proprietary binaries during runtime is an anti-pattern for resilient server-side tooling.

By trapping library failures at runtime and falling back to native OS utilities, you build a zero-downtime integration pipeline that performs under system constraints without exposing internal targets or credentials.