<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: azizam techno</title>
    <description>The latest articles on DEV Community by azizam techno (@mubsira).</description>
    <link>https://dev.to/mubsira</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3896726%2F7775f6b9-0200-4429-b685-1cd90567594f.png</url>
      <title>DEV Community: azizam techno</title>
      <link>https://dev.to/mubsira</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mubsira"/>
    <language>en</language>
    <item>
      <title>Stop bundling SheetJS for simple Excel exports: a 5 KB alternative</title>
      <dc:creator>azizam techno</dc:creator>
      <pubDate>Tue, 28 Apr 2026 13:02:58 +0000</pubDate>
      <link>https://dev.to/mubsira/stop-bundling-sheetjs-for-simple-excel-exports-a-5-kb-alternative-kpb</link>
      <guid>https://dev.to/mubsira/stop-bundling-sheetjs-for-simple-excel-exports-a-5-kb-alternative-kpb</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I open-sourced &lt;a href="https://github.com/MubsiraAnalytics/mini-xlsx" rel="noopener noreferrer"&gt;&lt;code&gt;mini-xlsx&lt;/code&gt;&lt;/a&gt;, a one-function, zero-dependency XLSX builder for the browser. Around 5 KB minified. No SheetJS, no ExcelJS, no JSZip. It writes real &lt;code&gt;.xlsx&lt;/code&gt; files that open cleanly in Excel, LibreOffice Calc, Google Sheets and Apple Numbers.&lt;/p&gt;

&lt;p&gt;If your only requirement is to &lt;strong&gt;output&lt;/strong&gt; Excel files (no reading, no formulas, no charts), you almost certainly do not need a 600 KB library to do it. This article walks through how I got there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;I was finishing &lt;a href="https://www.mubsiraanalytics.com/business/" rel="noopener noreferrer"&gt;Mubsira Business&lt;/a&gt;, a free offline-first invoice and expense tracker for freelancers. Tax season was the next milestone, and I needed a "send to accountant" button. One click, one file, everything inside: invoices, expenses, payments, clients, summary. Multi-sheet Excel was the obvious format because every accountant I have ever worked with lives in Excel.&lt;/p&gt;

&lt;p&gt;The obvious library was SheetJS. It is excellent, very battle-tested, and I have used it on other projects without complaint. But three things stopped me from reaching for it this time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The whole app is around 50 KB gzipped. Adding SheetJS would have roughly quadrupled the bundle.&lt;/li&gt;
&lt;li&gt;The app is offline-first. A bigger bundle means a slower service worker install, which means a worse first-run experience for the exact users I care about (people opening the app on a laptop in a cafe with patchy wifi).&lt;/li&gt;
&lt;li&gt;Most of SheetJS's surface area is for &lt;strong&gt;reading&lt;/strong&gt; Excel files, with all the legacy &lt;code&gt;.xls&lt;/code&gt;, encrypted-workbook and edge-case parsing that implies. I needed none of that.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I went looking at what an &lt;code&gt;.xlsx&lt;/code&gt; file actually is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an .xlsx file actually is
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;.xlsx&lt;/code&gt; file is a ZIP archive with a small set of XML files inside it. The full spec (OOXML, ECMA-376) is long, but the &lt;strong&gt;minimum&lt;/strong&gt; you need to produce a workbook that Excel will open without complaint is genuinely small. The directory layout looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-workbook.xlsx
├── [Content_Types].xml
├── _rels/
│   └── .rels
└── xl/
    ├── workbook.xml
    ├── sharedStrings.xml
    ├── styles.xml
    ├── _rels/
    │   └── workbook.xml.rels
    └── worksheets/
        ├── sheet1.xml
        └── sheet2.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven files. None of them are large. None of them are conceptually difficult. The roles are roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;[Content_Types].xml&lt;/code&gt;&lt;/strong&gt; declares the MIME type of every part inside the zip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;_rels/.rels&lt;/code&gt;&lt;/strong&gt; points the package at its main document (&lt;code&gt;xl/workbook.xml&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;xl/workbook.xml&lt;/code&gt;&lt;/strong&gt; lists the sheets in the workbook and their display names.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;xl/_rels/workbook.xml.rels&lt;/code&gt;&lt;/strong&gt; maps the sheet ids to the actual XML files on disk and to &lt;code&gt;sharedStrings.xml&lt;/code&gt; and &lt;code&gt;styles.xml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;xl/sharedStrings.xml&lt;/code&gt;&lt;/strong&gt; is a deduplicated table of every string used in any cell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;xl/styles.xml&lt;/code&gt;&lt;/strong&gt; holds fonts, fills, number formats and the styling indices that cells reference by id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;xl/worksheets/sheetN.xml&lt;/code&gt;&lt;/strong&gt; is one file per sheet, containing the actual rows and cells.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you accept that this is the whole problem, the rest is just text generation and a ZIP wrapper.&lt;/p&gt;

&lt;h2&gt;
  
  
  The minimum viable XLSX
&lt;/h2&gt;

&lt;p&gt;Here is the function signature I landed on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;miniXlsx&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invoices&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Date&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Amount&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INV-001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ACME Corp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-04-24&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1250.00&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INV-002&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Globex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-04-25&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mf"&gt;890.50&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Expenses&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Date&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Vendor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Amount&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-04-20&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Office Depot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;42.99&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You give it an array of sheet definitions. It gives you back a &lt;code&gt;Uint8Array&lt;/code&gt; you can wrap in a &lt;code&gt;Blob&lt;/code&gt; and download. That is the whole API.&lt;/p&gt;

&lt;p&gt;Internally, four things have to happen.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The shared strings table
&lt;/h3&gt;

&lt;p&gt;Excel does not put strings inline in cells (well, it can, but the canonical way is not to). It builds a deduplicated string table once, and every string cell points at it by index. So before generating any sheet XML, I walk every header and every cell, and feed strings to a small &lt;code&gt;addStr&lt;/code&gt; helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;sstMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addStr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sstMap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasOwnProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sstMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sst&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;sst&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;sstMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you &lt;code&gt;xl/sharedStrings.xml&lt;/code&gt;, and a lookup table you can use later when emitting cells.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Sheet XML with auto type detection
&lt;/h3&gt;

&lt;p&gt;Excel cells are typed. A number cell and a string cell have completely different syntax. To keep the API one-line-friendly, I auto-detect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the value parses as a number, write it as a numeric cell.&lt;/li&gt;
&lt;li&gt;If the value matches &lt;code&gt;YYYY-MM-DD&lt;/code&gt;, convert it to an Excel date serial and tag it with the date number format.&lt;/li&gt;
&lt;li&gt;Otherwise, treat it as a string and write the shared-string index.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The date serial is the only mildly weird part. Excel measures dates as days since 1899-12-30, and it includes a fictional 1900-02-29 leap day for backwards compatibility with Lotus 1-2-3. So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;dateToSerial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;epoch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1899&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;epoch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;86400000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the imaginary 1900 leap day&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single &lt;code&gt;+1&lt;/code&gt; after day 59 is the kind of thing I would happily pay SheetJS to handle for me on a bigger project. On this one, six lines is fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A tiny styles.xml
&lt;/h3&gt;

&lt;p&gt;I wanted three things visually: a coloured header row, a frozen top row, and reasonable date and number formatting. That fits in roughly 15 lines of &lt;code&gt;styles.xml&lt;/code&gt;: two fonts (regular and bold-white), three fills (none, gray125, and a teal &lt;code&gt;FF0D9488&lt;/code&gt; for the header), two number formats (date and &lt;code&gt;#,##0.00&lt;/code&gt;), and four &lt;code&gt;cellXfs&lt;/code&gt; entries that combine them.&lt;/p&gt;

&lt;p&gt;Cells then reference styles by their index in &lt;code&gt;cellXfs&lt;/code&gt;. Header cells get &lt;code&gt;s="1"&lt;/code&gt;, date cells &lt;code&gt;s="2"&lt;/code&gt;, decimal cells &lt;code&gt;s="3"&lt;/code&gt;, everything else &lt;code&gt;s="0"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Frozen header is one line in the sheet XML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;sheetViews&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;sheetView&lt;/span&gt; &lt;span class="na"&gt;workbookViewId=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;pane&lt;/span&gt; &lt;span class="na"&gt;ySplit=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;topLeftCell=&lt;/span&gt;&lt;span class="s"&gt;"A2"&lt;/span&gt; &lt;span class="na"&gt;activePane=&lt;/span&gt;&lt;span class="s"&gt;"bottomLeft"&lt;/span&gt; &lt;span class="na"&gt;state=&lt;/span&gt;&lt;span class="s"&gt;"frozen"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/sheetView&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/sheetViews&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. A no-compression ZIP
&lt;/h3&gt;

&lt;p&gt;This is the part I was most nervous about. ZIP is not difficult, but the spec is long and full of historical baggage. The shortcut: the ZIP format defines a compression method &lt;code&gt;0&lt;/code&gt;, called STORED, which means "the file content is in the archive verbatim". No DEFLATE, no Huffman coding, no zlib. You just write the bytes.&lt;/p&gt;

&lt;p&gt;It turns out Excel, LibreOffice, Google Sheets and Numbers all accept STORED &lt;code&gt;.xlsx&lt;/code&gt; files without complaint. The only cost is that the resulting file is larger than a properly-compressed one, but for the kind of payload an in-browser export typically produces (a few hundred KB at most), the difference is rounding error.&lt;/p&gt;

&lt;p&gt;The full ZIP writer is one function, around 60 lines. It computes a CRC32 per file, writes a local header, writes the file bytes, accumulates a central directory, and finishes with an end-of-central-directory record. That is it. No external dependency, no DEFLATE implementation, nothing to bundle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;About 300 lines of source. Around 5 KB minified. Zero dependencies. Produces files that open without warnings or "recovered content" prompts in Excel 2016+, LibreOffice Calc 7+, Google Sheets, and Apple Numbers.&lt;/p&gt;

&lt;p&gt;In production it powers the "send to accountant" export at the bottom of &lt;a href="https://www.mubsiraanalytics.com/business/" rel="noopener noreferrer"&gt;Mubsira Business&lt;/a&gt;. One click bundles invoices, expenses, payments, clients and a summary into a five-sheet workbook the user can email to their accountant.&lt;/p&gt;

&lt;p&gt;You can play with it here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/MubsiraAnalytics/mini-xlsx" rel="noopener noreferrer"&gt;github.com/MubsiraAnalytics/mini-xlsx&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Demo: open &lt;code&gt;example.html&lt;/code&gt; in the repo, click the button, get a workbook.&lt;/li&gt;
&lt;li&gt;In production: &lt;a href="https://www.mubsiraanalytics.com/business/" rel="noopener noreferrer"&gt;mubsiraanalytics.com/business&lt;/a&gt;, create a couple of invoices, hit "send to accountant".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations (by design)
&lt;/h2&gt;

&lt;p&gt;I want to be clear about what this thing is &lt;strong&gt;not&lt;/strong&gt;, so nobody adopts it for the wrong job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No formulas. Cells hold values, not expressions.&lt;/li&gt;
&lt;li&gt;No images, charts, comments, merged cells or conditional formatting.&lt;/li&gt;
&lt;li&gt;No reading. It only writes.&lt;/li&gt;
&lt;li&gt;Not appropriate for very large workbooks (think tens of MB of data). The XML is built as a string in memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need any of that, use &lt;a href="https://sheetjs.com/" rel="noopener noreferrer"&gt;SheetJS&lt;/a&gt; or &lt;a href="https://github.com/exceljs/exceljs" rel="noopener noreferrer"&gt;ExcelJS&lt;/a&gt;. They are both excellent and they are doing genuinely hard work that I am happy to skip.&lt;/p&gt;

&lt;p&gt;But if your needs are "render some rows of data into a downloadable &lt;code&gt;.xlsx&lt;/code&gt; and move on", consider whether you actually need the heavyweight option. A lot of the time, you do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;A few things I took away from the build that I think generalise:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read the format spec before reaching for the library.&lt;/strong&gt; OOXML looks intimidating from the outside; the subset you actually need for output is tiny. Same is often true of PDF, ICS, vCard, and a surprising amount of "scary" binary formats.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;STORED ZIP is a real escape hatch.&lt;/strong&gt; If your bundle budget cannot fit a DEFLATE implementation, you may not need one. Most consumers of ZIP-based formats accept method 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The shared strings table is the unintuitive bit.&lt;/strong&gt; First time I tried this I wrote strings inline and Excel opened the file but stripped half of them. Once I pointed everything at &lt;code&gt;sharedStrings.xml&lt;/code&gt; the warnings went away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frozen header and autofilter are one-line wins.&lt;/strong&gt; Both add real usability for accountants and analysts and cost nothing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you give it a try and it works (or breaks) in an interesting way, I would love to hear about it on the &lt;a href="https://github.com/MubsiraAnalytics/mini-xlsx/issues" rel="noopener noreferrer"&gt;issue tracker&lt;/a&gt;. And if you are running into the same problem on your own product, the code is MIT, copy what you need.&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://www.mubsiraanalytics.com" rel="noopener noreferrer"&gt;Mubsira Analytics&lt;/a&gt; — small, offline-first tools for freelancers and accountants. The repo is &lt;a href="https://github.com/MubsiraAnalytics/mini-xlsx" rel="noopener noreferrer"&gt;github.com/MubsiraAnalytics/mini-xlsx&lt;/a&gt; — stars and PRs welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
