Skip to content

Commit c744be8

Browse files
authored
Image Lazy Loading & Enhanced Loading Experience (#1230)
* LoadingView and Image Lazy Loading - Add an X button to Loading View to allow exiting out of V spinner view early. Appears after 3 second delay (not shown when loading completes in timely manner). Shows for thread loading only i.e. Does not show for new post previews or private message loading. - Timeouts added to image loading. In case of failure set "Dead Image" badge similar to Dead Tweets - Fix ghost lottie loading by using locally provided lottie.js file. - iOS 15.4+ now accepts the loading="lazy" attribute. Using this for image element lazy loading. Tweets use IntersectionObserver method. - Added a retry option to Dead Tweet badge, similar to Dead Image badge
1 parent 1685cdc commit c744be8

File tree

10 files changed

+1237
-201
lines changed

10 files changed

+1237
-201
lines changed

App/Misc/HTMLRenderingHelpers.swift

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ import HTMLReader
66

77
extension HTMLDocument {
88

9+
// MARK: - Constants
10+
11+
/// Number of post images to load immediately before deferring to lazy loading.
12+
/// IMPORTANT: This value must match IMMEDIATELY_LOADED_IMAGE_COUNT constant in RenderView.js
13+
private static let immediatelyLoadedImageCount = 10
14+
15+
/// 1x1 transparent GIF used as placeholder for lazy-loaded images
16+
private static let transparentPixelPlaceholder = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
17+
18+
// MARK: - HTML Processing Methods
19+
920
/// Finds links that appear to be to Bluesky posts and adds a `data-bluesky-post` attribute to those links.
1021
func addAttributeToBlueskyLinks() {
1122
for a in nodes(matchingSelector: "a[href *= 'bsky.app']") {
@@ -138,22 +149,56 @@ extension HTMLDocument {
138149
- Turns all non-smiley `<img src=>` elements into `<a data-awful='image'>src</a>` elements (if linkifyNonSmiles == true).
139150
- Adds .awful-smile to smilie elements.
140151
- Rewrites URLs for some external image hosts that have changed domains and/or URL schemes.
152+
- Defers loading of post content images beyond the first 10 (lazy loading).
141153
*/
142154
func processImgTags(shouldLinkifyNonSmilies: Bool) {
155+
var postContentImageCount = 0
156+
143157
for img in nodes(matchingSelector: "img") {
144158
guard
145159
let src = img["src"],
146160
let url = URL(string: src)
147161
else { continue }
148-
162+
149163
let isSmilie = isSmilieURL(url)
150-
164+
151165
if isSmilie {
152166
img.toggleClass("awful-smile")
153-
} else if let postimageURL = fixPostimageURL(url) {
154-
img["src"] = postimageURL.absoluteString
155-
} else if let waffleURL = randomwaffleURLForWaffleimagesURL(url) {
156-
img["src"] = waffleURL.absoluteString
167+
} else {
168+
// Check if this is an avatar (has class="avatar")
169+
let isAvatar = img["class"]?.contains("avatar") ?? false
170+
171+
// Skip attachment.php files (require auth, handled elsewhere)
172+
let isAttachment = url.lastPathComponent == "attachment.php"
173+
174+
// Apply URL fixes first to get the final URL
175+
var finalURL = src
176+
if let postimageURL = fixPostimageURL(url) {
177+
finalURL = postimageURL.absoluteString
178+
} else if let waffleURL = randomwaffleURLForWaffleimagesURL(url) {
179+
finalURL = waffleURL.absoluteString
180+
}
181+
182+
// Check if this is a data: URI (inline data that shouldn't be lazy-loaded)
183+
let isDataURI = finalURL.starts(with: "data:")
184+
185+
// Determine whether to load immediately or defer based on image type and count
186+
if !isAvatar && !isAttachment && !isDataURI {
187+
// This is a post content image (not avatar, not smilie, not attachment)
188+
postContentImageCount += 1
189+
190+
if postContentImageCount > Self.immediatelyLoadedImageCount {
191+
// Defer loading for images beyond the immediately loaded count (browser handles lazy loading)
192+
img["loading"] = "lazy"
193+
img["src"] = finalURL
194+
} else {
195+
// Load immediately
196+
img["src"] = finalURL
197+
}
198+
} else {
199+
// Avatars, attachments, and data URIs always load immediately
200+
img["src"] = finalURL
201+
}
157202
}
158203

159204
if shouldLinkifyNonSmilies, !isSmilie {

0 commit comments

Comments
 (0)