Skip to content

How the comparison was measured ​

Every figure on ts-pptx vs PptxGenJS comes from scripts/comparison/snapshot.json, which is refreshed on release cadence, and nothing on either page is edited by hand. This page is the method behind those figures and the full tables they summarise.

Measured on 2026-09-21: ts-pptx 3.7.0 built from this repository, against pptxgenjs 4.0.1 installed from npm (published 2025-06-26).

The corpus ​

The corpus is 22 deck intents. Each one states an intent ("a slide that enters with a push transition"), and each library expresses that intent in its own idiom. Transcribing one library's calls into the other is how a comparison gets rigged, so the two arms of a probe deliberately do not have to look alike. Both decks are then opened, and the part the probe names is read for the token it names.

Four outcomes are possible, per probe per library:

OutcomeMeaning
emittedthe token is present in the named part
absentan API exists, and the output does not carry the token
no APInothing in the public surface expresses the intent
errorthe build threw

no API is the only one of the four that is a claim rather than a reading, so it is checked rather than trusted: that library's shipped bundle is searched for the token, and a hit fails the measurement run unless the corpus carries a written reason for it. Those reasons are printed under the table they belong to.

Two things a reader should price in. The corpus is ours, so it was chosen by an interested party. It is kept honest in two specific ways: it carries a probe neither library can satisfy, and the set of probes upstream emits and ts-pptx does not is reported below even when it is empty, so an empty set is a stated result rather than something a reader has to infer from a gap. A pull request that adds a probe is welcome, including one ts-pptx fails.

Construct coverage ​

Of 22 probes, ts-pptx emitted 21 and pptxgenjs emitted 10.

"Looked for" is the token the harness reads for, in the part named beside it. It is the OOXML element in every case but one, where the intent is speaker notes and the token is the note text itself. Porting from PptxGenJS prints the calls behind every row.

Shared baseline ​

IntentLooked forPartts-pptxpptxgenjs
Text run<a:t>ppt/slides/slide1.xmlemittedemitted
Table<a:tbl>ppt/slides/slide1.xmlemittedemitted
Raster image<p:pic>ppt/slides/slide1.xmlemittedemitted
Bar chart<c:barChart>ppt/charts/chart1.xmlemittedemitted
External hyperlink<a:hlinkClickppt/slides/slide1.xmlemittedemitted
User-defined slide master<p:phppt/slides/slide1.xmlemittedemitted
Sections<p14:sectionLstppt/presentation.xmlemittedemitted
Speaker notesprobe noteppt/notesSlides/notesSlide1.xmlemittedemitted
Preset-geometry shape<a:prstGeomppt/slides/slide1.xmlemittedemitted
Slide background colour<p:bg>ppt/slides/slide1.xmlemittedemitted

Motion ​

IntentLooked forPartts-pptxpptxgenjs
Slide transition<p:transitionppt/slides/slide1.xmlemittedno API
Build animation on a shape<p:timing>ppt/slides/slide1.xmlemittedno API

Embedding ​

IntentLooked forPartts-pptxpptxgenjs
Embedded OLE object<p:oleObjppt/slides/slide1.xmlemittedno API
3D modelam3d:model3dppt/slides/slide1.xmlemittedno API
Embedded font face<p:embeddedFontLst>ppt/presentation.xmlemittedno API

Shapes ​

IntentLooked forPartts-pptxpptxgenjs
Connector between shapes<p:cxnSp>ppt/slides/slide1.xmlemittedno API

Text ​

IntentLooked forPartts-pptxpptxgenjs
Inline equation<a14:mppt/slides/slide1.xmlemittedno API

Fills ​

IntentLooked forPartts-pptxpptxgenjs
Gradient shape fill<a:gradFillppt/slides/slide1.xmlemittedno API
  • Gradient shape fill, pptxgenjs: appears only inside the bundled Office theme XML, which no API parameterises.

Tables ​

IntentLooked forPartts-pptxpptxgenjs
3D bevel on a table cell<a:cell3Dppt/slides/slide1.xmlemittedno API

Charts ​

IntentLooked forPartts-pptxpptxgenjs
Funnel chart (chartEx)<cx:chart>ppt/charts/chartEx1.xmlemittedno API
IntentLooked forPartts-pptxpptxgenjs
Slide Zoom tilepslz:sldZmppt/slides/slide2.xmlemittedno API

Diagrams ​

IntentLooked forPartts-pptxpptxgenjs
SmartArt diagram (write side)<dgm:relIdsppt/slides/slide1.xmlno APIno API

The shared baseline is the control group. A corpus holding only constructs one side cannot produce would prove that the corpus was chosen, not that the libraries differ, so 10 of the probes are ones both libraries are expected to pass. A failure there fails the measurement run instead of becoming a row on this page.

No probe in this corpus is emitted by pptxgenjs and not by ts-pptx.

Emitted by neither library: SmartArt diagram (write side).

Schema validity ​

This validates the decks this corpus builds, not either library in general. A deck no probe builds is not covered by any of it, and a library can be perfectly correct on everything these probes never touch.

Every deck the corpus built was passed through the Open XML SDK validator (3.5.1) at the Microsoft365 conformance target: the same oracle, and the same target, that this project's own test:schema suite uses.

ts-pptxpptxgenjs
Decks validated2110
Decks with no error210
Errors012
Intents with no deck to validate112

The last row is the denominator a validity count needs. A library that builds fewer decks has fewer decks to be wrong in, and reading the error counts without it would reward not having an API.

There is no warning column. This validator reports a single severity, so a zero in a second column would be a number nobody measured.

What failed in the pptxgenjs decks ​

Distinct diagnostics rather than a raw error total. One fault repeated across every deck, and that many unrelated faults, are different facts about a library, and a total on its own cannot tell them apart.

DiagnosticPartDecks
Sch_UnexpectedElementContentExpectingComplex/ppt/presentation.xml10
Sch_UnexpectedElementContentExpectingComplex/ppt/charts/chart1.xml1
Sch_AttributeValueDataTypeDetailed/ppt/presentation.xml1
  • /ppt/presentation.xml: The element has unexpected child element 'http://schemas.openxmlformats.org/presentationml/2006/main:notesMasterIdLst'. List of possible elements expected: <http://schemas.openxmlformats.org/presentationml/2006/main:notesSz>.
  • /ppt/charts/chart1.xml: The element has unexpected child element 'http://schemas.openxmlformats.org/drawingml/2006/chart:axId'.
  • /ppt/presentation.xml: The attribute 'id' has invalid value '{d1284d43-bd99-70d1-24f1-515cd268a88d}'. The Pattern constraint failed. The expected pattern is \{[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\}.

Package hygiene ​

What a consumer gets. Each library was installed on its own into an empty directory, upstream from the registry and ts-pptx from a pack of this working tree, so nothing here is measured against a development checkout with its dependencies hoisted flat.

ts-pptxpptxgenjsDifference
Installed size, with dependencies10.7 MiB6.7 MiB+61%
Installed size, the package alone6.1 MiB2.5 MiB+145%
Runtime dependencies, transitive318-83%

The last column is ts-pptx measured against pptxgenjs, so a positive number is ours costing more and a negative one is ours costing less. It is a percentage of the pptxgenjs figure rather than a difference in bytes, because two of its rows are in mebibytes and the third is a count, and a reader comparing them needs a number that does not change meaning between rows.

ts-pptx installs larger than pptxgenjs despite carrying fewer dependencies. The largest share of that weight is source maps: dist/ ships a .js.map beside every module, and each one embeds the original TypeScript. The unminified .js is the next largest share. No consumer build keeps either, which is why the bundled figures below are much closer together than the installed ones.

ts-pptxpptxgenjs
Runtime dependencies, direct@xmldom/xmldom, fflate, opentype.js@types/node, https, image-size, jszip
Entry points., ./inspect, ./measure, ./read, ./script, ./math, ./zip, ./html, ./families, ./node, ./browser.
Module formatsesmcjs, esm
engines.node>=24not declared

What a bundled program costs ​

Each row is a whole deck both libraries build: 5 consumer programs, from the smallest one anyone writes up to one using every construct the shared baseline above shows both of them emitting. Each is written in its own idiom on both sides, and the calls behind every row are on porting from PptxGenJS.

  • Hello world. One slide with one text box.
  • Text deck. A defined master, two sections, formatted and bulleted text, a hyperlink, a slide background and speaker notes.
  • Table deck. A titled slide and a bordered table with a header row, fixed column widths and per-cell options.
  • Chart deck. Three charts on three slides: a column chart with value labels, a two-series line chart, and a pie chart with percentages.
  • Full deck. Every construct the shared baseline covers, in one deck: master, sections, background, text, hyperlink, notes, a preset shape, an image, a table and a chart.
Programts-pptxpptxgenjsDifference
Hello world104.1 KiB123.2 KiB-16%
Text deck104.4 KiB123.6 KiB-16%
Table deck104.3 KiB123.4 KiB-16%
Chart deck104.3 KiB123.5 KiB-16%
Full deck104.6 KiB123.8 KiB-15%

The column is nearly flat, and that is the result. From hello world to full deck, ts-pptx grows by 0.5 KiB and pptxgenjs by 0.5 KiB, which is about what the programs' own literals weigh. Neither library splits along feature lines: importing either one costs almost everything it will ever cost, and the deck written afterwards is close to free. So a hello world was never a flattering measurement of either library, and a consumer weighing bundle size is choosing between two roughly fixed costs rather than between two slopes.

Both columns construct the library the way every consumer of pptxgenjs constructs it, with the class that carries everything. ts-pptx has a lower floor than that, reached by composing a presentation from only the construct families a program uses, and smaller bundles carries those figures. It is deliberately not a row here: pptxgenjs has no counterpart to compose, so the cell beside it would be empty and the percentage would be comparing two different programs.

Every program is identical in intent on both sides. Each is bundled with esbuild for the browser, minified, and gzipped at level 9, following the conventions scripts/bundle-size-ratchet.mjs documents, with one difference that matters. The ratchet never bundles, so it cannot drop unreachable code and its figures are an upper bound on what the package ships; this bundles and does tree-shake, because a consumer's build is precisely the thing being compared here. The two sets of numbers will not agree, and neither is wrong.

Code splitting is on, so each figure is the entry chunk: what the program pays before its first line runs. Chunks a bundler defers behind a dynamic import are not counted, because a program that never takes that path never fetches them.

Each program is run against both libraries before it is bundled. A bundler compiles a call that does not exist, and a misspelled method comes out as a smaller bundle rather than as an error, because the tree-shaker keeps less: running the programs first is what stops a typo being published here as a saving.

Generation time ​

How long each library takes to turn a deck into bytes: the same decks the bundle table above weighs, plus three larger ones built for this measurement alone, because the largest program up there is three slides and a clock has almost nothing to see in it. The calls behind every row are on porting from PptxGenJS.

A .pptx is a zip, so the compression setting is not a detail of this measurement, it is the measurement. The two libraries do not default to the same one. ts-pptx deflates unless told not to. pptxgenjs passes no compression option to JSZip on the outputType path, and JSZip stores by default. Its own compression argument is honoured on the stream and browser paths and ignored on the one in between, which is the path writeFile takes in Node. Timing the two default calls against each other would compare deflating with not deflating and report the difference as a library being slow, so both tables below are matched pairs.

Compressed ​

Both libraries asked for a compressed deck, which is what a consumer writing a file they intend to keep gets. This is the table that matters.

Deckts-pptxpptxgenjsDifference
Hello world4.4 ms6.9 ms-36%
Text deck5.9 ms8.0 ms-26%
Table deck5.3 ms6.9 ms-23%
Chart deck10 ms12 ms-19%
Full deck6.2 ms8.2 ms-24%
50 slides85 ms87 ms-2%
200 slides291 ms347 ms-16%
500 slides716 ms952 ms-25%

Stored, the control ​

Both libraries asked not to compress. The zip drops out of the measurement, leaving each library's own work: building the XML and assembling the package.

Deckts-pptxpptxgenjsDifference
Hello world1.3 ms0.9 ms+47%
Text deck1.7 ms1.2 ms+38%
Table deck1.8 ms0.9 ms+94%
Chart deck5.2 ms3.6 ms+44%
Full deck3.3 ms2.3 ms+45%
50 slides45 ms34 ms+33%
200 slides185 ms133 ms+39%
500 slides486 ms378 ms+29%

The two tables point in opposite directions, and that is the finding. Stored, ts-pptx is slower on every deck, by 46% on average, so our XML generation and package assembly cost more than upstream's. Compressed, ts-pptx is faster on every deck, by 21% on average, because fflate deflates faster than JSZip does and the compressor dominates the total. A consumer writing a file they intend to keep gets the first table. A consumer who has turned compression off gets the second, and should know that is where we are behind.

The measurement is a median over repeated rounds, taken after a warm-up that is thrown away, with the two libraries interleaved and the order alternated so that a machine which slows down mid-run cannot hand either column a result it did not earn. Building the deck and writing it are both inside the clock; constructing the presentation object is not.

The milliseconds belong to the machine that took them and do not transfer; the ratios mostly do. These were taken on Intel(R) Core(TM) Ultra 5 235U (14 cores) under Node v24.20.0 on win32, on 2026-09-21. Repeating a run on the same machine moves a difference by a few points in either direction, so read the columns for their direction and rough size rather than for their last digit.

Project health ​

The health table describes how the two projects are run, not what either one emits. Stars and downloads measure adoption, adoption measures history as much as merit, and none of it belongs in the same table as a construct a library does or does not write. This is how each figure in it is taken.

The last commit on the default branch is reported rather than the repository's last push, which the same API offers and which counts activity on any branch. The two disagree for pptxgenjs by several months, and reporting the later one would say something the default branch does not support.

Line counts come from the same walk on both sides: every code file under src/, raw lines with comments and blanks included, and test lines are spec files plus anything under a test directory, counted once each. No normalisation makes two libraries formatted to different rules comparable, and a large part of the ts-pptx figure is the documentation comments the bundled sizes above shed. Read it as an order of magnitude for how much there is to maintain, and as nothing at all about whether it is good.

The empty pptxgenjs test row is what this walk can see, and it is not the same claim as untested. That repository documents a manual, demo-driven process instead, which nothing measured here can weigh. The row is about an automated suite, and the coverage figure beside it exists for ts-pptx only because there is a suite to instrument.

ts-pptx is published under two names carrying the same bytes, pptx-ts and @shbernal/ts-pptx. The download figure is their sum (pptx-ts 365, @shbernal/ts-pptx 1,415), because either name alone understates the total, and the canonical name alone happens to understate it by most.

The pptxgenjs column shows no npm release since 2025-06-26 and no commit on master since 2025-06-26. That is what the two APIs report, and it is all these pages say about it: from outside, a stable library that has stopped needing changes looks exactly like one between maintainers, and this measurement cannot tell them apart. It is worth weighing either way, next to 233 open issues and 65 open pull requests.