github kean/Nuke 14.0.0-beta.2
Nuke 14.0 (Beta 2)

pre-release3 hours ago

Nuke 14 plays animated images, records where every image task spent its time, and loads images into any view with ImageLoader. It also fixes more than 30 bugs and raises the minimum platforms by one major version.

Nuke 14 requires Swift 6.2 and Xcode 26.0. The Migration Guide covers the upgrade, including what the compiler can't report.

Animated Images

LazyImage and LazyImageView play GIF, APNG, animated WebP, and HEIC and AVIF sequences with no setup. Nuke plays them itself because the pipeline has something to contribute in terms of performance, and most of it is about caching:

  • The animation is parsed once, off the main thread, and cached with the image, so a cell that scrolls back is ready the moment it appears
  • An animation that fits in memory is decoded once and replayed. One that doesn't plays out of a window of three frames. The shared frame pool takes up to 5% of the device's memory, capped at 128 MB, on top of ImageCache, and holds the smallest animations whole first
  • Views showing the same animation share one decoder and one set of frames, at the same size or close to it, and start on the same frame
  • Frames are decoded no larger than the view displays them, and a view nobody is watching holds two frames

WebKit, more than any other player, was the reference for how playback behaves. Frames are never skipped, so a stalled main thread stretches the animation instead of making it jump. A delay under 11 ms becomes 0.1 s, and a file with no loop count plays once. The clock is held to 30 Hz in Low Power Mode, for the same reasons WebKit does it. See the Animated Images article.

LazyImage(url: URL(string: "https://example.com/cat.gif"))

The repository now includes a demo app: open Nuke.xcodeproj and run the NukeDemo scheme. Its Animated Images screen shows the frame buffer, decode times, and dropped frames over a live animation, and Animation Lab plays up to 36 animations at once against the pool's budget.

  • Add AnimatedImage, AnimatedImageView, AnimatedImagePlayer, AnimatedImageFramePool, and LazyImageState/animatedImage to NukeUI, and ImageContainer/animation to Nuke – #958
  • ImageDecoders/Default parses every animated image it recognizes once, and attaches the result along with the encoded ImageContainer/data. ImagePipeline/Configuration-swift.struct/isAnimatedImageParsingEnabled turns it off – #958
  • A decoder registered with ImageDecoderRegistry can attach an AnimatedImageSource and an AnimatedImageFrameDecoding to play a format Image I/O can't read – #958
  • ImagePipeline/DataCachePolicy/storeEncodedImages stores APNG, animated WebP, and HEIC and AVIF sequences as their original data, like GIF, instead of as their first frame – #1098

Diagnostics

Set isDiagnosticsEnabled and every task finishes with an ImageTask/metrics record: where the image came from, how long each stage took and how long it waited for a queue, what the download cost, and whether another task shared the work. The record is Codable, and printing it gives a timeline of the load. While diagnostics are on, the pipeline also sends the tasks, jobs, and stages as os_signpost intervals. See the Performance Guide.

let pipeline = ImagePipeline {
    $0.isDiagnosticsEnabled = true
}

let task = pipeline.imageTask(with: url)
let image = try await task.image
print(task.metrics!)

A feed image, resized for the cell, on a host the app hadn't talked to yet:

ImageTask #2 "feed" · success · 140.9 ms · from network
url:         https://cdn.example.com/photos/1024.jpg
processors:  com.github.kean/nuke/resize?s=(900.0, 900.0),cm=.aspectFill,crop=false,upscale=false
priority:    normal
image:       1350×900 · jpeg · 4.9 MB in memory
transfer:    317 KB
pipeline:    44A3F653
time:        network 93.6 ms (66%) · process 42.5 ms (30%) · cache 0.5 ms · decode 0.3 ms · queue 0.1 ms · other 3.8 ms (3%)

pending                              <0.1 ms  ▏                          started at 17:24:14.677
j4 loadImage [resize]               139.5 ms  ████████████████████  99%
├─ memoryLookup                      <0.1 ms  ▏                          miss · key aa429747
├─ diskLookup                         0.3 ms  ▏                          miss · key f6d8540b
├─ j5 loadImage                      96.3 ms  ██████████████        68%
│  ├─ memoryLookup                   <0.1 ms  ▏                          miss · key 38c326ae
│  ├─ diskLookup                      0.1 ms  ▏                          miss · key 19d1c77c
│  └─ j6 fetchOriginalImage          96.0 ms  ██████████████        68%
│     ├─ j7 fetchOriginalData        95.2 ms  ██████████████        68%
│     │  ├─ download                 93.6 ms  ██████████████        66%  network · 317 KB · HTTP 200 · first byte 50.3 ms · session #2
│     │  │  └─ networkLoad           93.1 ms  ██████████████        66%  HTTP 200 · h2 · TLS 1.3 · 151.101.1.1 · sent 164 bytes · received 318 KB
│     │  │     ├─ blocked             1.4 ms  ░                      1%
│     │  │     ├─ domainLookup        1.0 ms   ▏                     1%
│     │  │     ├─ connect            10.0 ms   █                     7%
│     │  │     ├─ secureConnection   21.0 ms    ███                 15%
│     │  │     ├─ request            <0.1 ms       ▏
│     │  │     ├─ waiting            16.1 ms       ░░               11%
│     │  │     └─ response           43.3 ms         ███████        31%
│     │  └─ diskStore                <0.1 ms                ▏            317 KB · key 19d1c77c
│     └─ decode                       0.3 ms                ▏            ImageDecoders.Default · jpeg 1440×960
├─ process                           42.6 ms                ██████  30%  jpeg 1350×900
└─ memoryStore                        0.1 ms                     ▕       key aa429747
total                               140.9 ms                             finished at 17:24:14.818

To see what the tasks are doing without changing any code, launch with NUKE_DIAGNOSTICS_LOG=1, and every task that finishes logs a line to the console. To find out why a task hasn't finished, pipeline.diagnostics.snapshot() reports the tasks in flight, the queues, and the memory cache, with diagnostics on or off, and po ImagePipeline.shared.diagnostics prints it at a breakpoint. The new Troubleshooting article explains what the lines mean.

  • Add ImageTask/metrics, recorded when ImagePipeline/Configuration-swift.struct/isDiagnosticsEnabled is set – #960
  • Add ImageRequest/UserInfoKey/labelKey, a short name for the role of a request, recorded in ImageTask/Metrics/label – #960
  • Add ImageDiagnosticsStringEnum, the Codable conformance the diagnostics enums share – #960
  • ImageRequest/Priority now conforms to Codable – #960
  • Add ImagePipeline/Configuration-swift.struct/isDiagnosticsLoggingEnabled, which launching with NUKE_DIAGNOSTICS_LOG also sets, and ImageTask/Metrics/summary, the line it logs – #1048
  • Add ImagePipeline/Diagnostics-swift.struct/snapshot() – #1048

ImageLoader

ImageLoader in NukeUI loads images into UIImageView and NSImageView, or into any object that draws its state. It replaces NukeExtensions, which is now soft-deprecated.

ImageLoader.load(url, into: imageView)
  • Add ImageLoader to NukeUI: it loads images into UIImageView and NSImageView, or into any object that draws its ImageLoader/State, and replaces NukeExtensions, which is now soft-deprecated – #1039
  • Add ImageLoader/load(_:) that takes an async closure – #1040
  • Add ImageLoader/reload() – #1045
  • Add a public initializer to ImageLoader/State – #1046
  • ImageLoader/Presentation/render(_:in:) is now public – #1044

Quality

The test suite more than doubled, and writing it uncovered many of the bugs fixed in this release. NukeUI went from 63% to 95% line coverage.

Version Source lines Tests Test lines Coverage
Nuke 14 7,358 1,698 21,562 98.6%
Nuke 13.2 5,011 937 10,740 96.4%
NukeUI 14 3,006 540 7,600 94.7%
NukeUI 13.2 804 80 978 63.4%

Measured on macOS. Source and test lines exclude blank lines and comments.

AI Agents

Nuke ships with two skills for coding agents: nuke-migrate upgrades an app to Nuke 14, including what the compiler can't report, and nuke-optimize measures and fixes slow images. There's nothing to install. Ask an agent to follow one:

Upgrade this app to Nuke 14. Follow https://raw.githubusercontent.com/kean/Nuke/main/skills/nuke-migrate/SKILL.md
Speed up the images in this app. Follow https://raw.githubusercontent.com/kean/Nuke/main/skills/nuke-optimize/SKILL.md

In Claude Code, you can also install them as a plugin, and they load on their own when they're relevant:

/plugin marketplace add kean/Nuke
/plugin install nuke@nuke
  • Add the nuke-migrate and nuke-optimize skills – #1048
  • Add llms.txt, llms-full.txt, and llms-api.txt, generated from the articles, the skills, and the sources, and included in every checkout at the version it resolved – #1048
  • Add AGENTS.md and make test-one for working on Nuke itself – #1048

Requirements

  • Minimum required platforms: iOS 16.0, tvOS 16.0, watchOS 9.0, macOS 13.0, visionOS 1.0 – #904

API Changes

  • Remove previously deprecated APIs: ImagePipelineDelegate typealias, ImageRequest.imageId, ImageRequest.UserInfoKey.imageIdKey, ImageRequest.UserInfoKey.scaleKey, ImageRequest.UserInfoKey.thumbnailKey, ImagePipeline.Configuration.maximumDecodedImageSize, and ImageDecodingContext.maximumDecodedImageSize – #907
  • Remove the soft-deprecated userInfo parameter from the ImageRequest initializers; it's still available as a property – #909
  • ImageRequest.scale and ImageRequest.ThumbnailOptions.init(maxPixelSize:) now use CGFloat – #910
  • Remove Combine support: ImagePipeline.imagePublisher(with:) and FetchImage.load(_:) that takes a publisher. Use the Async/Await APIs instead – #911
  • Add ImageTask/status, one snapshot of everything that changes while a task runs. ImageTask/state and ImageTask/currentProgress are deprecated – #913
  • Remove ImageTask/Event/started, which ImageTask/events never yielded, and add ImagePipeline/Delegate/imageTaskDidStart(_:pipeline:) in its place – #914
  • ImageTask now conforms to Identifiable – #922
  • DataCache is now Sendable instead of @unchecked Sendable. DataCache/flush() and DataCache/sweep() are now async, and DataCache/queue and flush(for:) are removed – #925
  • Add AssetType/avif and detect the avif and avis brands, which ImageContainer/type previously reported as nil – #926
  • DataLoader/Error now conforms to Sendable – #933
  • ImageProcessors/CoreImageFilter/Error is now Sendable instead of @unchecked Sendable: its cases carry a filter name and descriptions instead of the CIFilter, CIImage, and [String: Any] payloads – #934
  • ImageProcessors/CoreImageFilter now takes [String: any Sendable] filter parameters instead of [String: Any] – #934
  • ImageLoadingOptions and its nested ContentModes, TintColors, and Transition now conform to Sendable, and the ImageLoadingOptions/Transition/custom(_:) closure is now @MainActor @Sendable – #935
  • ImageDecodingContext/init(request:data:isCompleted:urlResponse:cacheType:previewPolicy:isAnimatedImageParsingEnabled:) now takes previewPolicy and isAnimatedImageParsingEnabled – #936
  • ImageDecoders/Empty/assetType is now public – #940
  • TaskQueue/maxConcurrentOperationCount and the matching initializer parameter are renamed to maxConcurrentTaskCount; the old names are deprecated – #942
  • ImagePipeline/Delegate/willCache(data:image:for:pipeline:) is now async and returns the data to store instead of taking a completion closure. Return nil to prevent caching – #943
  • NukeUI, NukeExtensions, and NukeVideo now re-export Nuke, so importing them is enough. The NukeUI.ImageRequest typealias is removed – #946
  • ImagePipeline/Delegate now declares the isolation of every method instead of documenting it: willLoadData, willCache, imageTaskDidStart, and imageTask(_:didReceiveEvent:pipeline:) run on ImagePipelineActor, and the rest are nonisolated – #945
  • NukeUI now surfaces the typed ImagePipeline/Error instead of any Error: FetchImage/result, LazyImageState/result, LazyImageState/error, and the onCompletion and onFailure callbacks – #949
  • FetchImage/progress and LazyImageState/progress now return ImageTask/Progress, which FetchImage publishes itself, instead of the nested FetchImage.Progress observable object that needed its own @ObservedObject. The old type is deprecated – #953
  • Add ImagePipeline/Error/isCancelled – #952
  • Nuke_ImageDisplaying is renamed to ImageDisplaying, is no longer @objc, and takes an ImageContainer: nuke_display(image:data:) becomes nuke_display(_:). A conformance declared in an extension can no longer be overridden by a subclass – #958
  • ImagePipeline/Configuration-swift.struct/imageDecodingQueue now runs 2 concurrent tasks instead of 1, matching imageDecompressingQueue. It carries the pixel work of a thumbnail request, which decodes and downsamples in one step and skips decompression – #962
  • Remove the rate limiter. ImagePipeline/Configuration-swift.struct/isRateLimiterEnabled is deprecated and no longer does anything – #963
  • ImageTask and ImageDecoders/Video are now Sendable instead of @unchecked Sendable – #965
  • Add TaskQueue/reservedTaskCount: the number of slots that the work with a priority lower than .normal can't take – #981
  • Replace the global ImagePipeline/Configuration-swift.struct/isSignpostLoggingEnabled with the per-pipeline ImagePipeline/Configuration-swift.struct/signpostLog: signposts are sent while diagnostics are on, unless it's set to nil – #995
  • LazyImage/priority(_:) with nil now keeps the request's own priority instead of resetting it to .normal – #1034
  • LazyImage/processors(_:) with nil now keeps the request's own processors; pass [] to remove them. nil in processors(_:) and priority(_:) also takes back an earlier call – #1035
  • NukeExtensions now depends on NukeUI – #1038
  • LazyImageView/imageTask is now read-only – #1039
  • FetchImage/load(_:) and LazyImageView/request keep the running task when they load the same image again, instead of restarting the download – #1043
  • ImagePipeline/Error and DataLoader/Error now conform to LocalizedError: localizedDescription returns their description, a technical English message, instead of a generic one – #1048
  • ImagePipeline/Error descriptions now say what was received, such as a text/html page sent with a 200, along with a redacted URL – #1048
  • Add unavailable stubs for the most used APIs removed in Nuke 12 and 13, such as ImageRequestConvertible, LazyImage(source:), and onSuccess, so the compiler error names the replacement – #1048
  • Add View/imagePipeline(_:) and EnvironmentValues/imagePipeline to set the pipeline of every LazyImage in a view hierarchy. LazyImage/pipeline(_:) takes precedence – #1094

Performance

Across the 44 benchmarks comparable with 13.2, main is 1.6% faster by geometric mean. The largest slowdowns are creatingRequestsWithOptions +59%, storingRequestInCollections +50%, lazyImageViewMainThreadPerformance +37%, and cacheOversizedEntryRejection +29%. Measured on the iOS 26.5 simulator with library evolution off.

  • ImagePipeline/Configuration-swift.struct/dataLoadingQueue runs the requests below .normal priority in at most 3 of its 6 slots to ensure there are always slots available regardless of the number of low-priority tasks, like prefetching – #981
  • ImageCache hits are 5% faster – #975
  • Loading an image with ImagePipeline/Configuration-swift.struct/dataCache set makes 10 fewer allocations per download – #971
  • ImagePipeline/Error is now 8 bytes instead of 97, which shrinks ImageTask/Event from 99 bytes to 26 – #967
  • LazyImage checks whether its request changed up to 5× faster on every body update when it keeps the same request – #969
  • Subscribing to ImageTask/events no longer spawns a Task, which makes it 1.2–1.9× faster – #980
  • ImageCache hits for requests with processors are 1.5× faster – #972
  • LazyImage checks whether a request with processors changed 1.7× faster – #978
  • Creating processor identifiers, which go in every data cache key, is 2.5× faster, and generating filenames 1.5× – #979
  • Decompression is up to 45% faster on the iOS simulator: ImagePipeline/Configuration-swift.struct/isUsingPrepareForDisplay, which also applies to tvOS and visionOS, is now enabled by default – #990

Bug Fixes

  • Fix ImageTask/events producing an empty stream when the task finishes before the subscription lands, as it does on a memory cache hit – #916
  • Fix a data race on the DataCache configuration properties – #925
  • Fix DataCache.init reading and decoding its metadata file on the calling thread, typically the main thread during app launch – #925
  • Fix DataCache updating the entry access date on the calling thread on every disk cache hit – #927
  • Fix a data race on DataLoader/prefersIncrementalDelivery – #928
  • Fix a data race on ImagePrefetcher/didComplete – #929
  • Fix DataCache sweeping only once per launch instead of every DataCache/sweepInterval – #930
  • Fix DataCache/sweep() not recording the sweep date, so the next scheduled sweep ran again within DataCache/sweepInterval – #932
  • Fix DataCache writing entries non-atomically, so a read that arrived while the same key was being overwritten could return a truncated file – #937
  • Fix VideoPlayerView not resuming a looping video on macOS when the view is added back to a window – #951
  • Fix ImagePrefetcher/didComplete not being called when stopping prefetching cancels the last outstanding request – #968
  • Fix ImageTask/events, ImageTask/progress, and ImageTask/previews missing previews and progress updates sent before the subscription lands – #980
  • Fix a cancelled download keeping its ImagePipeline/Configuration-swift.struct/dataLoadingQueue slot when a custom DataLoading doesn't call completion after cancel() – #989
  • Fix ImagePipeline/data(for:) and ImagePrefetcher/Destination/diskCache downloading a thumbnail or processed request again when the original data is in the disk cache – #1004
  • Fix ImageRequest/Options/returnCacheDataDontLoad failing requests with processors even when the original image is cached – #1007
  • Fix an image stored in a full ImageCache being evicted right away when every cached image had been read – #1011
  • Fix processed images and thumbnails built from an original in the disk cache never being stored in the disk cache under the .automatic and .storeAll policies – #1016
  • Fix ImageRequest/Options/skipDataLoadingQueue being ignored when another request for the same resource is already waiting in the queue – #1005
  • Fix a data race on DataLoader/delegate – #998
  • Fix LazyImage restarting a running request when its priority changes or when the view reappears after .lowerPriority, discarding what it had downloaded – #1034
  • Fix setting FetchImage/priority or LazyImageView/priority back to nil leaving the running task at the overridden priority – #1034
  • Fix DataCache skipping the scheduled sweeps when the recorded last sweep date is in the future, e.g. after the device clock was corrected – #1024
  • Fix LazyImage starting a new request with the pipeline, onStart, and onCompletion from when the view last appeared, and ignoring a change of the pipeline alone – #1037
  • Fix FetchImage/isLoading remaining true when the task passed to onStart is cancelled directly – #1039
  • Fix ImagePrefetcher skipping images that only have a progressive preview in the memory cache – #1022
  • Fix ImagePrefetcher/priority updates being applied out of order, leaving the outstanding prefetches at a stale priority – #1022
  • Fix ImageDecoders/Default previews ignoring the EXIF orientation, so a rotated progressive image snapped a quarter turn when the download completed – #1029
  • Fix ImageDecoders/Default decoding full-size progressive previews for a request with ImageRequest/thumbnail set – #1029
  • Fix ImageDecoders/Default not attaching ImageContainer/UserInfoKey/scanNumberKey to the GIF preview, or to the final GIF that followed it – #1029
  • Fix ImageProcessors/GaussianBlur returning a corrupted near-black image and allocating gigabytes of scratch memory for a radius of about 1544 or more – #1021
  • Fix ImageProcessors/GaussianBlur converting wide-gamut (Display P3) images to device RGB – #1021
  • Fix ImageProcessors/Resize/init(width:unit:upscale:) and init(height:unit:upscale:) capping the other dimension at 9999, which shrank tall or wide images below the requested size – #1021

Documentation

  • Add an article on using Nuke from Objective-C – #948
  • Fix the code samples in the SwiftUI, UIKit, Objective-C, and Performance articles, and compile them in CI – #955
  • Add an article on animated images and update the format documentation – #958
  • Add a Troubleshooting article – #1048
  • Fix the samples that drifted from the API in the image processing, SwiftUI, and caching articles, and in the LazyImage documentation – #1048
  • Correct the Performance Guide: thumbnails save peak memory on formats other than JPEG, not time – #1048

Internal

  • Replace the hand-rolled os_unfair_lock_t allocations with OSAllocatedUnfairLock – #908
  • TaskSubscription is now ~Copyable, so the compiler rejects unsubscribing twice – #961
  • Fix the crop documentation of ImageProcessors/Resize saying the flag has no effect with .aspectFill; it has no effect with .aspectFit – #1021

Don't miss a new Nuke release

NewReleases is sending notifications on new releases.