diff --git a/src/api/Backend.tsx b/src/api/Backend.tsx
index bdedb4a3fb3c03e0b03003a4b44669ef388dd601..e0368d0eb4fb75c9cea13c634b6608278db905f8 100644
--- a/src/api/Backend.tsx
+++ b/src/api/Backend.tsx
@@ -67,6 +67,10 @@ export class BackendImpl {
     private csrfToken?: string;
     public isPrivileged = false;
 
+    protected fetchImpl(url: string, init?: RequestInit): Promise<Response> {
+        return fetch(url, init);
+    }
+
     assetUrl(): string {
         return "https://video.fsmpi.rwth-aachen.de/files";
     }
@@ -135,7 +139,7 @@ export class BackendImpl {
             init.cache = "no-cache";
         }
         init.credentials = "include";
-        return fetch(url, init);
+        return this.fetchImpl(url, init);
     }
 
     setCsrfToken(csrfToken?: string) {
@@ -505,3 +509,38 @@ export class BackendImpl {
             });
     }
 }
+
+export class SmartCacheBackend extends BackendImpl {
+    protected fetchImpl(url: string, init?: RequestInit) {
+        let shouldCache = true;
+        if (!SmartCacheBackend.isSupported()) shouldCache = false;
+        if (init && init.cache === "no-cache") shouldCache = false;
+        if (!url.startsWith(this.baseUrl())) shouldCache = false;
+        if (!shouldCache) return super.fetchImpl(url, init);
+
+        const cacheName = `api-v1-${this.isPrivileged ? "privileged" : "public"}`;
+        return caches.open(cacheName).then((cache) => {
+            return cache.match(url).then((response) => {
+                if (response) {
+                    console.log(`Cache hit (${cacheName}) on ${url}`);
+                    return response;
+                } else {
+                    return fetch(url, init).then((response) => {
+                        cache.put(url, response.clone());
+                        return response;
+                    });
+                }
+            });
+        });
+    }
+
+    static isSupported() {
+        return window.isSecureContext && "caches" in window;
+    }
+}
+
+export class InvalidBackend extends BackendImpl {
+    protected fetchImpl(url: string, init?: RequestInit): Promise<Response> {
+        return Promise.reject(new Error("Invalid backend"));
+    }
+}
diff --git a/src/components/BackendProvider.tsx b/src/components/BackendProvider.tsx
index fb1c5699f438c894cab2cf0c2370dfc507d7acf3..b8dd54064dc8f3f4dd3201213b993cfd7809cf8a 100644
--- a/src/components/BackendProvider.tsx
+++ b/src/components/BackendProvider.tsx
@@ -1,12 +1,12 @@
-import { BackendImpl } from "@/api/Backend";
+import { Backend, InvalidBackend, SmartCacheBackend } from "@/api/Backend";
 import { createContext, useContext, useState } from "react";
 
 import type React from "react";
 
-const BackendContext = createContext<BackendImpl>(new BackendImpl());
+const BackendContext = createContext<Backend>(new InvalidBackend());
 
 export function RealBackendProvider({ children }: { children: React.ReactNode }) {
-    let [state, _] = useState(() => new BackendImpl());
+    let [state, _] = useState(() => new SmartCacheBackend());
 
     return <BackendContext.Provider value={state}>{children}</BackendContext.Provider>;
 }