diff --git a/Model/Cache/ChannelPlaylistsCacheModel.swift b/Model/Cache/ChannelPlaylistsCacheModel.swift
new file mode 100644
index 00000000..1b2f0ac6
--- /dev/null
+++ b/Model/Cache/ChannelPlaylistsCacheModel.swift
@@ -0,0 +1,70 @@
+import Cache
+import Foundation
+import Logging
+import SwiftyJSON
+
+struct ChannelPlaylistsCacheModel {
+    static let shared = ChannelPlaylistsCacheModel()
+    let logger = Logger(label: "stream.yattee.cache.channel-playlists")
+
+    static let diskConfig = DiskConfig(name: "channel-playlists")
+    static let memoryConfig = MemoryConfig()
+
+    let storage = try! Storage<String, JSON>(
+        diskConfig: Self.diskConfig,
+        memoryConfig: Self.memoryConfig,
+        transformer: CacheModel.jsonTransformer
+    )
+
+    func storePlaylist(playlist: ChannelPlaylist) {
+        let date = CacheModel.shared.iso8601DateFormatter.string(from: Date())
+        logger.info("STORE \(playlistCacheKey(playlist.id)) -- \(date)")
+        let feedTimeObject: JSON = ["date": date]
+        let playlistObject: JSON = ["playlist": playlist.json.object]
+        try? storage.setObject(feedTimeObject, forKey: playlistTimeCacheKey(playlist.id))
+        try? storage.setObject(playlistObject, forKey: playlistCacheKey(playlist.id))
+    }
+
+    func retrievePlaylist(_ id: ChannelPlaylist.ID) -> ChannelPlaylist? {
+        logger.info("RETRIEVE \(playlistCacheKey(id))")
+
+        if let json = try? storage.object(forKey: playlistCacheKey(id)).dictionaryValue["playlist"] {
+            return ChannelPlaylist.from(json)
+        }
+
+        return nil
+    }
+
+    func getPlaylistsTime(_ id: ChannelPlaylist.ID) -> Date? {
+        if let json = try? storage.object(forKey: playlistTimeCacheKey(id)),
+           let string = json.dictionaryValue["date"]?.string,
+           let date = CacheModel.shared.iso8601DateFormatter.date(from: string)
+        {
+            return date
+        }
+
+        return nil
+    }
+
+    func getFormattedPlaylistTime(_ id: ChannelPlaylist.ID) -> String {
+        if let time = getPlaylistsTime(id) {
+            let isSameDay = Calendar(identifier: .iso8601).isDate(time, inSameDayAs: Date())
+            let formatter = isSameDay ? CacheModel.shared.dateFormatterForTimeOnly : CacheModel.shared.dateFormatter
+            return formatter.string(from: time)
+        }
+
+        return ""
+    }
+
+    func clear() {
+        try? storage.removeAll()
+    }
+
+    private func playlistCacheKey(_ playlist: ChannelPlaylist.ID) -> String {
+        "channelplaylists-\(playlist)"
+    }
+
+    private func playlistTimeCacheKey(_ playlist: ChannelPlaylist.ID) -> String {
+        "\(playlistCacheKey(playlist))-time"
+    }
+}
diff --git a/Model/ChannelPlaylist.swift b/Model/ChannelPlaylist.swift
index c10104ac..444dc10e 100644
--- a/Model/ChannelPlaylist.swift
+++ b/Model/ChannelPlaylist.swift
@@ -1,4 +1,5 @@
 import Foundation
+import SwiftyJSON
 
 struct ChannelPlaylist: Identifiable {
     var id: String = UUID().uuidString
@@ -7,4 +8,26 @@ struct ChannelPlaylist: Identifiable {
     var channel: Channel?
     var videos = [Video]()
     var videosCount: Int?
+
+    var json: JSON {
+        [
+            "id": id,
+            "title": title,
+            "thumbnailURL": thumbnailURL?.absoluteString ?? "",
+            "channel": channel?.json.object ?? "",
+            "videos": videos.map { $0.json.object },
+            "videosCount": String(videosCount ?? 0)
+        ]
+    }
+
+    static func from(_ json: JSON) -> Self {
+        ChannelPlaylist(
+            id: json["id"].stringValue,
+            title: json["title"].stringValue,
+            thumbnailURL: json["thumbnailURL"].url,
+            channel: Channel.from(json["channel"]),
+            videos: json["videos"].arrayValue.map { Video.from($0) },
+            videosCount: json["videosCount"].int
+        )
+    }
 }
diff --git a/Model/Playlist.swift b/Model/Playlist.swift
index dc74e159..95a8fd31 100644
--- a/Model/Playlist.swift
+++ b/Model/Playlist.swift
@@ -40,8 +40,6 @@ struct Playlist: Identifiable, Equatable, Hashable {
     }
 
     var json: JSON {
-        let dateFormatter = ISO8601DateFormatter()
-
         return [
             "id": id,
             "title": title,
@@ -53,8 +51,6 @@ struct Playlist: Identifiable, Equatable, Hashable {
     }
 
     static func from(_ json: JSON) -> Self {
-        let dateFormatter = ISO8601DateFormatter()
-
         return .init(
             id: json["id"].stringValue,
             title: json["title"].stringValue,
@@ -71,4 +67,8 @@ struct Playlist: Identifiable, Equatable, Hashable {
     func hash(into hasher: inout Hasher) {
         hasher.combine(id)
     }
+
+    var channelPlaylist: ChannelPlaylist {
+        ChannelPlaylist(id: id, title: title, videos: videos, videosCount: videos.count)
+    }
 }
diff --git a/Shared/Playlists/PlaylistsView.swift b/Shared/Playlists/PlaylistsView.swift
index b01816ec..90d9bf06 100644
--- a/Shared/Playlists/PlaylistsView.swift
+++ b/Shared/Playlists/PlaylistsView.swift
@@ -94,19 +94,19 @@ struct PlaylistsView: View {
         }
         .onAppear {
             model.load()
-            resource?.load()
+            loadResource()
         }
         .onChange(of: accounts.current) { _ in
             model.load(force: true)
-            resource?.load()
+            loadResource()
         }
         .onChange(of: currentPlaylist) { _ in
             channelPlaylist.clear()
             userPlaylist.clear()
-            resource?.load()
+            loadResource()
         }
         .onChange(of: model.reloadPlaylists) { _ in
-            resource?.load()
+            loadResource()
         }
         #if os(iOS)
         .refreshControl { refreshControl in
@@ -154,7 +154,7 @@ struct PlaylistsView: View {
         #if !os(macOS)
         .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
             model.load()
-            resource?.loadIfNeeded()
+            loadResource()
         }
         #endif
         #if !os(tvOS)
@@ -168,6 +168,26 @@ struct PlaylistsView: View {
         #endif
     }
 
+    func loadResource() {
+        loadCachedResource()
+        resource?.load()
+            .onSuccess { response in
+                if let playlist: Playlist = response.typedContent() {
+                    ChannelPlaylistsCacheModel.shared.storePlaylist(playlist: playlist.channelPlaylist)
+                }
+            }
+    }
+
+    func loadCachedResource() {
+        if !selectedPlaylistID.isEmpty,
+           let cache = ChannelPlaylistsCacheModel.shared.retrievePlaylist(selectedPlaylistID)
+        {
+            DispatchQueue.main.async {
+                self.channelPlaylist.replace(cache)
+            }
+        }
+    }
+
     #if os(iOS)
         var playlistsMenu: some View {
             Menu {
diff --git a/Shared/Views/PlaylistVideosView.swift b/Shared/Views/PlaylistVideosView.swift
index dfd6c577..1f482287 100644
--- a/Shared/Views/PlaylistVideosView.swift
+++ b/Shared/Views/PlaylistVideosView.swift
@@ -2,7 +2,7 @@ import Siesta
 import SwiftUI
 
 struct PlaylistVideosView: View {
-    let playlist: Playlist
+    var playlist: Playlist
 
     @ObservedObject private var accounts = AccountsModel.shared
     var player = PlayerModel.shared
@@ -43,6 +43,24 @@ struct PlaylistVideosView: View {
         return resource
     }
 
+    func loadResource() {
+        loadCachedResource()
+        resource?.load()
+            .onSuccess { response in
+                if let playlist: Playlist = response.typedContent() {
+                    ChannelPlaylistsCacheModel.shared.storePlaylist(playlist: playlist.channelPlaylist)
+                }
+            }
+    }
+
+    func loadCachedResource() {
+        if let cache = ChannelPlaylistsCacheModel.shared.retrievePlaylist(playlist.id) {
+            DispatchQueue.main.async {
+                self.channelPlaylist.replace(cache)
+            }
+        }
+    }
+
     var videos: [Video] {
         contentItems.compactMap(\.video)
     }
@@ -55,10 +73,10 @@ struct PlaylistVideosView: View {
         VerticalCells(items: contentItems)
             .onAppear {
                 guard contentItems.isEmpty else { return }
-                resource?.load()
+                loadResource()
             }
             .onChange(of: model.reloadPlaylists) { _ in
-                resource?.load()
+                loadResource()
             }
         #if !os(tvOS)
             .navigationTitle("\(playlist.title) Playlist")
diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj
index 779514e8..c1ff91f1 100644
--- a/Yattee.xcodeproj/project.pbxproj
+++ b/Yattee.xcodeproj/project.pbxproj
@@ -537,6 +537,9 @@
 		3776925229463C310055EC18 /* PlaylistsCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3776925129463C310055EC18 /* PlaylistsCacheModel.swift */; };
 		3776925329463C310055EC18 /* PlaylistsCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3776925129463C310055EC18 /* PlaylistsCacheModel.swift */; };
 		3776925429463C310055EC18 /* PlaylistsCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3776925129463C310055EC18 /* PlaylistsCacheModel.swift */; };
+		377692562946476F0055EC18 /* ChannelPlaylistsCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377692552946476F0055EC18 /* ChannelPlaylistsCacheModel.swift */; };
+		377692572946476F0055EC18 /* ChannelPlaylistsCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377692552946476F0055EC18 /* ChannelPlaylistsCacheModel.swift */; };
+		377692582946476F0055EC18 /* ChannelPlaylistsCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377692552946476F0055EC18 /* ChannelPlaylistsCacheModel.swift */; };
 		3776ADD6287381240078EBC4 /* Captions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3776ADD5287381240078EBC4 /* Captions.swift */; };
 		3776ADD7287381240078EBC4 /* Captions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3776ADD5287381240078EBC4 /* Captions.swift */; };
 		3776ADD8287381240078EBC4 /* Captions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3776ADD5287381240078EBC4 /* Captions.swift */; };
@@ -1256,6 +1259,7 @@
 		37737785276F9858000521C1 /* Windows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Windows.swift; sourceTree = "<group>"; };
 		3776924D294630110055EC18 /* ChannelAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelAvatarView.swift; sourceTree = "<group>"; };
 		3776925129463C310055EC18 /* PlaylistsCacheModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistsCacheModel.swift; sourceTree = "<group>"; };
+		377692552946476F0055EC18 /* ChannelPlaylistsCacheModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPlaylistsCacheModel.swift; sourceTree = "<group>"; };
 		3776ADD5287381240078EBC4 /* Captions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Captions.swift; path = Model/Captions.swift; sourceTree = SOURCE_ROOT; };
 		377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = "<group>"; };
 		377ABC3F286E4AD5009C986F /* InstancesManifest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstancesManifest.swift; sourceTree = "<group>"; };
@@ -2022,6 +2026,7 @@
 			children = (
 				3738535329451DC800D2D0CB /* BookmarksCacheModel.swift */,
 				37F5E8B9291BEF69006C15F5 /* CacheModel.swift */,
+				377692552946476F0055EC18 /* ChannelPlaylistsCacheModel.swift */,
 				377F9F7E2944175F0043F856 /* FeedCacheModel.swift */,
 				3776925129463C310055EC18 /* PlaylistsCacheModel.swift */,
 				377F9F7A294403F20043F856 /* VideosCacheModel.swift */,
@@ -3160,6 +3165,7 @@
 				37EBD8C627AF26B300F1C24B /* AVPlayerBackend.swift in Sources */,
 				375E45F527B1976B00BA7902 /* MPVOGLView.swift in Sources */,
 				37BE0BCF26A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
+				377692562946476F0055EC18 /* ChannelPlaylistsCacheModel.swift in Sources */,
 				3769C02E2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
 				37F5E8B6291BE9D0006C15F5 /* URLBookmarkModel.swift in Sources */,
 				37579D5D27864F5F00FD0B98 /* Help.swift in Sources */,
@@ -3300,6 +3306,7 @@
 				378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */,
 				37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */,
 				37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */,
+				377692572946476F0055EC18 /* ChannelPlaylistsCacheModel.swift in Sources */,
 				374924E1292126A00017D862 /* VideoDetailsToolbar.swift in Sources */,
 				37F5E8BB291BEF69006C15F5 /* CacheModel.swift in Sources */,
 				3765788A2685471400D4EA09 /* Playlist.swift in Sources */,
@@ -3563,6 +3570,7 @@
 				37AAF29226740715007FC770 /* Channel.swift in Sources */,
 				37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
 				376527BD285F60F700102284 /* PlayerTimeModel.swift in Sources */,
+				377692582946476F0055EC18 /* ChannelPlaylistsCacheModel.swift in Sources */,
 				371B7E5E27596B8400D21217 /* Comment.swift in Sources */,
 				37732FF22703A26300F04329 /* AccountValidationStatus.swift in Sources */,
 				3756C2AC2861151C00E4B059 /* NetworkStateModel.swift in Sources */,