<?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: Digital Web</title>
    <description>The latest articles on DEV Community by Digital Web (@digital_web).</description>
    <link>https://dev.to/digital_web</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4046887%2F3658dc59-5337-4efe-a10c-d4cb720b7396.png</url>
      <title>DEV Community: Digital Web</title>
      <link>https://dev.to/digital_web</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/digital_web"/>
    <language>en</language>
    <item>
      <title>CompressPower</title>
      <dc:creator>Digital Web</dc:creator>
      <pubDate>Sat, 25 Jul 2026 13:14:25 +0000</pubDate>
      <link>https://dev.to/digital_web/compresspower-27fj</link>
      <guid>https://dev.to/digital_web/compresspower-27fj</guid>
      <description>&lt;p&gt;Most "compress your image" sites still work the same way: you upload the file, a server does the work, you download the result. For a 4 MB photo that's two network trips and a queue, plus your file sitting on someone else's disk.&lt;/p&gt;

&lt;p&gt;You don't need any of it. The browser has shipped everything required for this since about 2012.&lt;/p&gt;

&lt;p&gt;The basic version&lt;/p&gt;

&lt;p&gt;async function compress(file, quality = 0.8) {&lt;br&gt;
  const bitmap = await createImageBitmap(file)&lt;br&gt;
  const canvas = new OffscreenCanvas(bitmap.width, bitmap.height)&lt;br&gt;
  canvas.getContext('2d').drawImage(bitmap, 0, 0)&lt;br&gt;
  return canvas.convertToBlob({ type: 'image/webp', quality })&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;That's the whole thing. createImageBitmap decodes off the main thread, OffscreenCanvas means you can run it in a Worker, and convertToBlob re-encodes at whatever quality you ask for. Ten lines, no dependencies, no server.&lt;/p&gt;

&lt;p&gt;Where it gets annoying&lt;/p&gt;

&lt;p&gt;Quality is not linear. Going from 0.9 to 0.8 on a JPEG typically saves 30 to 40% of the bytes with almost no visible difference. Going from 0.6 to 0.5 saves maybe 8% and starts putting rings around high contrast edges. If your UI exposes a quality slider, users will drag it to 0.3 and then complain about artifacts.&lt;/p&gt;

&lt;p&gt;A target-size loop usually beats a quality slider:&lt;/p&gt;

&lt;p&gt;async function compressToTarget(file, maxBytes) {&lt;br&gt;
  let lo = 0.4, hi = 0&lt;br&gt;
  return best&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Six binary search steps gets you close enough and runs in well under a second for typical photos.&lt;/p&gt;

&lt;p&gt;Canvas strips EXIF. Including orientation. Photos from phones come out rotated and users assume your tool broke them. You have to read the orientation tag before you draw and apply the transform yourself.&lt;/p&gt;

&lt;p&gt;Canvas also strips the color profile. Wide gamut images from newer phones get re-tagged as sRGB and come out visibly duller. There's no clean fix in the Canvas path. If this matters you need a WASM encoder like mozjpeg or libwebp compiled with profile preservation.&lt;/p&gt;

&lt;p&gt;Alpha channels. Converting a transparent PNG to JPEG gives you a black background unless you fill the canvas white first. Obvious in hindsight, easy to ship as a bug.&lt;/p&gt;

&lt;p&gt;Big files kill the tab. A 50 MP image is roughly 200 MB as raw RGBA in memory. Do this on the main thread with three files at once and the tab dies. Worker plus a queue, not Promise.all.&lt;/p&gt;

&lt;p&gt;The real argument for client-side&lt;/p&gt;

&lt;p&gt;It's not performance, though it is faster. It's that the file never leaves the machine.&lt;/p&gt;

&lt;p&gt;If someone is compressing a scanned passport to fit an upload limit, "processed in your browser" is a genuinely different product from "uploaded to our servers and deleted within 24 hours." One of those is a promise. The other is architecture.&lt;/p&gt;

&lt;p&gt;A working example if you want to see the pattern in production rather than build it: compresspower.com does image compression, format conversion between JPG, PNG and WebP, and PDF merge and split, all in-browser with no upload step.&lt;/p&gt;

&lt;p&gt;Worth opening devtools on the network tab while you use one of these. If you see your file going out over the wire, the privacy copy on the landing page is decorative.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>performance</category>
      <category>webperf</category>
    </item>
    <item>
      <title>16 states, 16 tax rates: modeling German property transfer tax without hardcoding a mess</title>
      <dc:creator>Digital Web</dc:creator>
      <pubDate>Sat, 25 Jul 2026 13:11:23 +0000</pubDate>
      <link>https://dev.to/digital_web/16-states-16-tax-rates-modeling-german-property-transfer-tax-without-hardcoding-a-mess-1jjc</link>
      <guid>https://dev.to/digital_web/16-states-16-tax-rates-modeling-german-property-transfer-tax-without-hardcoding-a-mess-1jjc</guid>
      <description>&lt;p&gt;If you ever build anything touching German real estate, you will meet Grunderwerbsteuer. It's the property transfer tax, it's set per state, and the rates are not close to each other. Bavaria and Saxony sit at 3.5%. NRW, Brandenburg, Saarland, Schleswig-Holstein and Thuringia sit at 6.5%. The rest land somewhere in between.&lt;/p&gt;

&lt;p&gt;On a 400,000 euro apartment that's 14,000 versus 26,000. Same transaction, 12,000 euro difference, decided by a line on a map.&lt;/p&gt;

&lt;p&gt;The trap: treating rates as constants&lt;/p&gt;

&lt;p&gt;The obvious first move is a lookup object:&lt;/p&gt;

&lt;p&gt;const GRUNDERWERBSTEUER = { BY: 0.035, NW: 0.065, /* ... */ }&lt;/p&gt;

&lt;p&gt;This is wrong within about a year, and wrong in a way that produces silently incorrect historical data. States change these rates, and they've each changed several times since the power was devolved in 2006. If a user saved an analysis two years ago and reopens it, recalculating with today's rate gives them a number that never existed.&lt;/p&gt;

&lt;p&gt;Rates are time-scoped facts, not constants:&lt;/p&gt;

&lt;p&gt;type RatePeriod = { from: string; to: string | null; rate: number }&lt;/p&gt;

&lt;p&gt;const RATES: Record = {&lt;br&gt;
  HH: [&lt;br&gt;
    { from: '2009-01-01', to: '2022-12-31', rate: 0.045 },&lt;br&gt;
    { from: '2023-01-01', to: null,         rate: 0.055 },&lt;br&gt;
  ],&lt;br&gt;
  // ...&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;function rateAt(state: StateCode, date: string) {&lt;br&gt;
  const p = RATES[state].find(r =&amp;gt; date &amp;gt;= r.from &amp;amp;&amp;amp; (!r.to || date &amp;lt;= r.to))&lt;br&gt;
  if (!p) throw new Error(&lt;code&gt;no rate for ${state} at ${date}&lt;/code&gt;)&lt;br&gt;
  return p.rate&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Every saved analysis stores its own date. Recalculation is then deterministic and reproducible, which is the whole point if anyone is making a six-figure decision off your output.&lt;/p&gt;

&lt;p&gt;Money in floats&lt;/p&gt;

&lt;p&gt;0.1 + 0.2  // 0.30000000000000004&lt;/p&gt;

&lt;p&gt;You know this. It still shows up, usually as a total that's off by one cent from the sum of its parts, and a user who notices it stops trusting every other number on the page. Integer cents internally, format at the boundary. Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }) for output.&lt;/p&gt;

&lt;p&gt;The part that's actually hard&lt;/p&gt;

&lt;p&gt;Transfer tax is the easy input. The hard part is that "yield" has no single definition. Bruttomietrendite uses the asking price. Nettomietrendite uses the total acquisition cost including all Kaufnebenkosten. These differ by 10 to 15%, and listing portals quote the flattering one.&lt;/p&gt;

&lt;p&gt;Then cashflow needs annuity amortization with a split between interest and principal, non-recoverable operating costs, a maintenance reserve, vacancy assumption, and AfA depreciation which itself depends on the building's construction year. Every one of those is a defensible assumption with a wide range, and the output is extremely sensitive to all of them.&lt;/p&gt;

&lt;p&gt;Which means the interesting design question is not the math. It's whether you show your assumptions. A calculator that returns "5.4% yield" with hidden defaults is worse than useless, because it looks authoritative. A calculator that shows the vacancy rate and maintenance reserve it used, and lets you change them, is a tool.&lt;/p&gt;

&lt;p&gt;If you want to see it built out rather than build it yourself, &lt;a href="https://www.investbud.de/" rel="noopener noreferrer"&gt;https://www.investbud.de/&lt;/a&gt; does this for German property, with the per-state transfer tax applied automatically and the cashflow broken down monthly and annually.&lt;/p&gt;

&lt;p&gt;Financial calculators are a good exercise, by the way. They look like CRUD and they're mostly domain modeling, which is the part most side projects let you skip.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
