DEV Community

Arun Mallikarjuna
Arun Mallikarjuna

Posted on AI-assisted

I replaced JasperReports with JavaScript – and it's 4 faster

ReportWright demo home page: headline

The ReportWright playground with the drag-and-drop designer open on a loan statement: a toolbox of report items on the left, the page with header, borrower fields, summary tiles and a repayment schedule table in the middle, and page, reuse and regional settings on the right.

The loan statement from the designer, rendered in Preview with real data: a

If you build business software, you've probably met report engines: JasperReports, SSRS, Crystal Reports. They're powerful, but they need a JVM or .NET, often a paid server, and they don't fit a JavaScript stack.

The JavaScript options weren't great either. You could print HTML through headless Chrome (slow, heavy, falls over on big reports), or hand-code layouts with a document builder like pdfmake.

So I built ReportWright: an open-source (MIT) report engine for JavaScript.

What it does

You describe a report in JSON (page bands, groups, tables, charts, expressions), give it data, and it renders:

  • PDF (including PDF/A for archiving and PDF/UA for accessibility)
  • Excel, Word and PowerPoint
  • interactive HTML, where groups expand and drill down inside the file

It runs in Node.js or the browser. There's a drag-and-drop designer, a viewer for React, Vue, Svelte and Angular, and a CLI.

A report in a few lines

npm i @reportwright/engine@next @reportwright/fonts@next

import fs from 'node:fs';
import { render, exportPdf, defaultFontStore } from '@reportwright/engine';

const report = {
  $schema: 'reportwright/report@1',
  name: 'Orders',
  page: { size: 'A4', margins: [36, 36, 36, 36] },
  dataSources: [{ name: 'src', type: 'json', data: { rows: [
    { customer: 'Acme', amount: 1200 },
    { customer: 'Globex', amount: 850 },
  ] } }],
  dataSets: [{ name: 'Orders', source: 'src', path: '$.rows' }],
  sections: { body: { items: [{
    type: 'table', dataSet: 'Orders', x: 0, y: 0, w: 523, h: 40,
    columns: [{ width: 360 }, { width: 163 }],
    header: [{ height: 18, cells: [{ value: 'Customer' }, { value: 'Amount' }] }],
    detail: [{ height: 16, cells: [{ value: '=Fields.customer' }, { value: '=Fields.amount', style: { format: 'N2' } }] }],
  }] } },
};

const fonts = defaultFontStore();
const model = await render(report, { fontStore: fonts });
fs.writeFileSync('orders.pdf', await exportPdf(model, { fonts }));
Enter fullscreen mode Exit fullscreen mode

Moving from SSRS or JasperReports

npx @reportwright/cli@next import ./my-reports --out ./imported
Enter fullscreen mode Exit fullscreen mode

It converts .rdl, .rdlc, .jrxml, BIRT and ActiveReports definitions, and writes a report of what carried over and what needs a manual fix.

How fast is it?

I benchmarked it on GitHub Actions (4 vCPU) against other engines, checking every output file.

Grouped report ReportWright JasperReports QuestPDF (.NET)
10,000 rows 0.91 s 3.9 s 2.7 s
100,000 rows 6.6 s 11.8 s 24.1 s
1,000 separate reports 8 s 33 s 26 s

A million rows streams in 53 s with flat memory. Against Python, the PDF library writes a million rows in 29 s versus ReportLab's 215 s.

Where it loses

I'd rather you hear it from me:

  • QuestPDF is about 2× faster on single invoices.
  • Go and Rust PDF writers are faster on tiny files, mostly because Node takes ~30 ms to start.
  • C tools like MuPDF and qpdf win quick edits such as merge and encrypt.

All results, wins and losses: https://github.com/MrArun005/reportwright/blob/bench-deep/bench/deep/RESULTS.md

Why JavaScript can be this fast

  • V8 compiles hot code to machine code, so tight loops (layout, text measuring) run fast.
  • The PDF writer streams pages out as they're done, so memory stays flat even at a million rows.
  • Binary work uses typed arrays, not millions of small objects.

Try it

It's free and MIT-licensed. If you have SSRS or Jasper reports that don't import well, please open an issue. Those are the most useful bug reports I can get.

Top comments (0)