diff --git a/build/build.js b/build/build.js
index b3c9aad4..c4df034f 100644
--- a/build/build.js
+++ b/build/build.js
@@ -23,6 +23,9 @@ rm('-rf', assetsPath)
 mkdir('-p', assetsPath)
 cp('-R', 'static/*', assetsPath)
 
+sed('-i', /'jxl_dec.js/, "'/node_modules/jxl.js/jxl_dec.js", 'node_modules/jxl.js/jxl.js')
+sed('-i', /"jxl_dec.wasm/, '"/node_modules/jxl.js/jxl_dec.wasm', 'node_modules/jxl.js/jxl_dec.js')
+
 webpack(webpackConfig, function (err, stats) {
   spinner.stop()
   if (err) throw err
diff --git a/build/dev-server.js b/build/dev-server.js
index 5acd0fed..ef275509 100644
--- a/build/dev-server.js
+++ b/build/dev-server.js
@@ -5,11 +5,15 @@ var path = require('path')
 var express = require('express')
 var webpack = require('webpack')
 var opn = require('opn')
+require('shelljs/global')
 var proxyMiddleware = require('http-proxy-middleware')
 var webpackConfig = process.env.NODE_ENV === 'testing'
   ? require('./webpack.prod.conf')
   : require('./webpack.dev.conf')
 
+sed('-i', /'jxl_dec.js/, "'/node_modules/jxl.js/jxl_dec.js", 'node_modules/jxl.js/jxl.js')
+sed('-i', /"jxl_dec.wasm/, '"/node_modules/jxl.js/jxl_dec.wasm', 'node_modules/jxl.js/jxl_dec.js')
+
 // default port where dev server listens for incoming traffic
 var port = process.env.PORT || config.dev.port
 // Define HTTP proxies to your custom API backend
diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js
index 2a3db96e..665eaa21 100644
--- a/build/webpack.base.conf.js
+++ b/build/webpack.base.conf.js
@@ -2,6 +2,8 @@ var path = require('path')
 var config = require('../config')
 var utils = require('./utils')
 var projectRoot = path.resolve(__dirname, '../')
+const CopyPlugin = require("copy-webpack-plugin");
+const WriteFilePlugin = require("write-file-webpack-plugin");
 var { VueLoaderPlugin } = require('vue-loader')
 
 var env = process.env.NODE_ENV
@@ -118,6 +120,14 @@ module.exports = {
     ]
   },
   plugins: [
-    new VueLoaderPlugin()
+    new VueLoaderPlugin(),
+    new CopyPlugin({
+      patterns: [
+        {
+          from: "node_modules/jxl.js/jxl*"
+        }
+      ]
+    }),
+    new WriteFilePlugin()
   ]
 }
diff --git a/package.json b/package.json
index 19a7e186..54a9c4af 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
     "diff": "3.5.0",
     "escape-html": "1.0.3",
     "js-cookie": "^3.0.1",
+    "jxl.js": "^1.0.0",
     "localforage": "1.10.0",
     "parse-link-header": "^2.0.0",
     "phoenix": "1.6.2",
@@ -63,6 +64,7 @@
     "chalk": "1.1.3",
     "chromedriver": "^107.0.3",
     "connect-history-api-fallback": "^2.0.0",
+    "copy-webpack-plugin": "^11.0.0",
     "cross-spawn": "^7.0.3",
     "css-loader": "^6.7.2",
     "custom-event-polyfill": "^1.0.7",
@@ -124,7 +126,8 @@
     "webpack-dev-middleware": "^5.3.3",
     "webpack-hot-middleware": "^2.25.1",
     "webpack-merge": "^5.8.0",
-    "workbox-webpack-plugin": "^6.5.4"
+    "workbox-webpack-plugin": "^6.5.4",
+    "write-file-webpack-plugin": "^4.5.1"
   },
   "engines": {
     "node": ">= 16.0.0",
diff --git a/src/App.js b/src/App.js
index d4b3b41a..ef67a6a4 100644
--- a/src/App.js
+++ b/src/App.js
@@ -53,6 +53,9 @@ export default {
   unmounted () {
     window.removeEventListener('resize', this.updateMobileState)
   },
+  mounted() {
+    import("./lib/jxl.js").then(jxl => jxl.startPolyfill());
+  },
   computed: {
     classes () {
       return [
diff --git a/src/lib/jxl.js b/src/lib/jxl.js
new file mode 100644
index 00000000..568b5293
--- /dev/null
+++ b/src/lib/jxl.js
@@ -0,0 +1,80 @@
+const config = {
+  useCache: true
+};
+
+let cache, workers = {};
+
+function imgDataToDataURL(img, imgData, isCSS) {
+  const jxlSrc = img.dataset.jxlSrc;
+  if (imgData instanceof Blob) {
+    const dataURL = URL.createObjectURL(imgData);
+    if (isCSS)
+      img.style.backgroundImage = 'url("' + dataURL + '")';
+    else
+      img.src = dataURL;
+  } else if ('OffscreenCanvas' in window) {
+    const canvas = new OffscreenCanvas(imgData.width, imgData.height);
+    workers[jxlSrc].postMessage({ canvas, imgData }, [canvas]);
+    workers[jxlSrc].addEventListener('message', m => {
+      if (m.data.url && m.data.blob) {
+        if (isCSS)
+          img.style.backgroundImage = 'url("' + m.data.url + '")';
+        else
+          img.src = m.data.url;
+        config.useCache && cache && cache.put(jxlSrc, new Response(m.data.blob));
+      }
+    });
+  } else {
+    const canvas = document.createElement('canvas');
+    canvas.width = imgData.width;
+    canvas.height = imgData.height;
+    canvas.getContext('2d').putImageData(imgData, 0, 0);
+    canvas.toBlob(blob => {
+      const dataURL = URL.createObjectURL(blob);
+      if (isCSS)
+        img.style.backgroundImage = 'url("' + dataURL + '")';
+      else
+        img.src = dataURL;
+      config.useCache && cache && cache.put(jxlSrc, new Response(blob));
+    }, 'image/jpeg');
+  }
+}
+
+
+async function decode(img, isCSS) {
+  const jxlSrc = img.dataset.jxlSrc = isCSS ? getComputedStyle(img).backgroundImage.slice(5, -2) : img.currentSrc;
+  img.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; // blank 1x1 image
+  if (config.useCache) {
+    try {
+      cache = cache || await caches.open('jxl');
+    } catch (e) { }
+    const cachedImg = cache && await cache.match(jxlSrc);
+    if (cachedImg) {
+      const cachedImgData = await cachedImg.blob();
+      requestAnimationFrame(() => imgDataToDataURL(img, cachedImgData, isCSS));
+      return;
+    }
+  }
+  const res = await fetch(jxlSrc);
+  const image = await res.arrayBuffer();
+  workers[jxlSrc] = new Worker('/node_modules/jxl.js/jxl_dec.js');
+  workers[jxlSrc].postMessage({ jxlSrc, image });
+  workers[jxlSrc].addEventListener('message', m => m.data.imgData && requestAnimationFrame(() => imgDataToDataURL(img, m.data.imgData, isCSS)));
+}
+
+function handleElement(el) {
+  if (el instanceof HTMLImageElement && el.src.endsWith('.jxl')) {
+    decode(el, false);
+    return;
+  } else if (el instanceof Element && getComputedStyle(el).backgroundImage.endsWith('.jxl)'))
+    decode(el, true);
+  el.childNodes.forEach(handleElement);
+}
+
+
+export function startPolyfill() {
+  new MutationObserver(mutations => mutations.forEach(mutation => {
+    mutation.addedNodes.forEach(handleElement);
+  })).observe(document.documentElement, { subtree: true, childList: true });
+  handleElement(document.documentElement); // Run the polyfill once on the full DOM
+}
diff --git a/yarn.lock b/yarn.lock
index bbceba0b..92cfd7cd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2732,7 +2732,7 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
     strip-ansi "^3.0.0"
     supports-color "^2.0.0"
 
-chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0:
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.0:
   version "2.4.2"
   resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz"
   integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -2994,6 +2994,18 @@ cookie@0.4.2, cookie@~0.4.1:
   resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz"
   integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
 
+copy-webpack-plugin@^11.0.0:
+  version "11.0.0"
+  resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a"
+  integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==
+  dependencies:
+    fast-glob "^3.2.11"
+    glob-parent "^6.0.1"
+    globby "^13.1.1"
+    normalize-path "^3.0.0"
+    schema-utils "^4.0.0"
+    serialize-javascript "^6.0.0"
+
 core-js-compat@^3.20.2, core-js-compat@^3.21.0:
   version "3.24.1"
   resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.24.1.tgz"
@@ -3187,7 +3199,7 @@ debug@4.3.1:
   dependencies:
     ms "2.1.2"
 
-debug@^3.2.7:
+debug@^3.1.0, debug@^3.2.7:
   version "3.2.7"
   resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz"
   integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
@@ -4006,7 +4018,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
 
-fast-glob@^3.2.12:
+fast-glob@^3.2.11, fast-glob@^3.2.12:
   version "3.2.12"
   resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
   integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
@@ -4084,6 +4096,11 @@ filelist@^1.0.1:
   dependencies:
     minimatch "^5.0.1"
 
+filesize@^3.6.1:
+  version "3.6.1"
+  resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
+  integrity sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==
+
 fill-range@^7.0.1:
   version "7.0.1"
   resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz"
@@ -4343,6 +4360,13 @@ glob-parent@^5.1.2, glob-parent@~5.1.2:
   dependencies:
     is-glob "^4.0.1"
 
+glob-parent@^6.0.1:
+  version "6.0.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
+  integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
+  dependencies:
+    is-glob "^4.0.3"
+
 glob-to-regexp@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
@@ -4440,6 +4464,17 @@ globby@^11.1.0:
     merge2 "^1.4.1"
     slash "^3.0.0"
 
+globby@^13.1.1:
+  version "13.1.2"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.2.tgz#29047105582427ab6eca4f905200667b056da515"
+  integrity sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==
+  dependencies:
+    dir-glob "^3.0.1"
+    fast-glob "^3.2.11"
+    ignore "^5.2.0"
+    merge2 "^1.4.1"
+    slash "^4.0.0"
+
 globjoin@^0.1.4:
   version "0.1.4"
   resolved "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz"
@@ -4452,7 +4487,7 @@ gonzales-pe@^4.3.0:
   dependencies:
     minimist "^1.2.5"
 
-graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9:
+graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9:
   version "4.2.10"
   resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz"
   integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
@@ -5314,6 +5349,11 @@ jsonpointer@^5.0.0:
   resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
   integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==
 
+jxl.js@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/jxl.js/-/jxl.js-1.0.0.tgz#e2c78e5c135d7d272c0d36f86b7f79de6f4b40b3"
+  integrity sha512-1br2vK02/9nYAbIEdrt7Qe6uRk5e+S0TGoRxUlxnGpXJYARfhFAfFHHArcQYjU4WMPNjLPci4oXBNsEUFVdEEQ==
+
 karma-coverage@1.1.2:
   version "1.1.2"
   resolved "https://registry.npmjs.org/karma-coverage/-/karma-coverage-1.1.2.tgz"
@@ -5679,7 +5719,7 @@ lodash.truncate@^4.4.2:
   resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
   integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==
 
-lodash@4.17.21, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4:
+lodash@4.17.21, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4:
   version "4.17.21"
   resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -6007,6 +6047,11 @@ mocha@3.5.3:
     mkdirp "0.5.1"
     supports-color "3.1.2"
 
+moment@^2.22.1:
+  version "2.29.4"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
+  integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
+
 ms@0.7.1:
   version "0.7.1"
   resolved "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
@@ -7414,6 +7459,11 @@ slash@^3.0.0:
   resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
   integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
 
+slash@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
+  integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
+
 slice-ansi@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
@@ -8661,6 +8711,15 @@ wrappy@1:
   resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
   integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
 
+write-file-atomic@^2.3.0:
+  version "2.4.3"
+  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481"
+  integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==
+  dependencies:
+    graceful-fs "^4.1.11"
+    imurmurhash "^0.1.4"
+    signal-exit "^3.0.2"
+
 write-file-atomic@^4.0.2:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd"
@@ -8669,6 +8728,19 @@ write-file-atomic@^4.0.2:
     imurmurhash "^0.1.4"
     signal-exit "^3.0.7"
 
+write-file-webpack-plugin@^4.5.1:
+  version "4.5.1"
+  resolved "https://registry.yarnpkg.com/write-file-webpack-plugin/-/write-file-webpack-plugin-4.5.1.tgz#aeeb68889194da5ec8a864667d46da9e00ee92d5"
+  integrity sha512-AZ7qJUvhTCBiOtG21aFJUcNuLVo2FFM6JMGKvaUGAH+QDqQAp2iG0nq3GcuXmJOFQR2JjpjhyYkyPrbFKhdjNQ==
+  dependencies:
+    chalk "^2.4.0"
+    debug "^3.1.0"
+    filesize "^3.6.1"
+    lodash "^4.17.13"
+    mkdirp "^0.5.1"
+    moment "^2.22.1"
+    write-file-atomic "^2.3.0"
+
 ws@~8.2.3:
   version "8.2.3"
   resolved "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz"