github kean/Nuke 14.0.0-rc.1
Nuke 14.0 (RC 1)

pre-releaseone hour 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 10 bugs and raises the minimum iOS, tvOS, macOS, and watchOS versions 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 GIF 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 late 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/animation 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/label, a short name for the role of a request, recorded in ImageTask/Metrics/label – #960
  • The diagnostics enums are Codable, and decode a case they don't know as .unknown – #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 94% line coverage.

Version Source lines Tests Test lines Coverage
Nuke 14 8,040 1,898 23,944 98.2%
Nuke 13.2 5,011 937 10,740 96.4%
NukeUI 14 3,315 653 9,225 93.9%
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
  • Add ImageProcessors/Crop, ImageProcessors/Flip, ImageProcessors/Rotate, and ImageProcessors/Tint, with ImageProcessing/crop(rect:unit:), ImageProcessing/flip(_:), ImageProcessing/rotate(degrees:), and ImageProcessing/tint(color:) – #1103
  • Add ImageDecoders/PDF, an opt-in decoder that draws a page of a PDF document at the size the request asks for, and AssetType/pdf. It isn't available on tvOS – #1107
  • Remove ImageDecoding/isAsynchronous: every decode runs on imageDecodingQueue – #1066
  • ImageDecoders/Video follows the preview policy, which is .disabled for a video by default, so a video no longer shows its first frame while it downloads unless the policy asks for it – #1082
  • The default DataLoader sets URLSessionTask/priority from the request's priority – #1085

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
  • LazyImage shows an image from the memory cache in its first body, with no placeholder before it, so a screen of cached images shows up 27% faster – #1093
  • DataCache starts writing once 16 MB is staged instead of holding the data for a second: peak growth for 120 12 MP JPEGs with ImagePipeline/Configuration-swift.struct/withDataCache is 60 MB instead of 261 MB on loopback, 63 MB instead of 86 MB at 48 MB/s, and unchanged at 12 MB/s – #1105

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 updating the entry access date on the calling thread on every disk cache hit – #927
  • Fix DataCache sweeping only once per launch instead of every DataCache/sweepInterval – #930
  • 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 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 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 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 ImageProcessors/Resize failing for a thin image whose short side scales below half a pixel instead of producing a one-pixel strip – #1020
  • Fix ImageProcessors/Circle and ImageProcessors/RoundedCorners drawing the border at half the requested width – #1020
  • Fix processors changing the point size of a high-DPI NSImage on macOS – #1020
  • Fix ImageRequest/ThumbnailOptions/init(size:unit:contentMode:) sizing the thumbnail of an EXIF-rotated image against the stored pixels on macOS, so it came out too large – #1020
  • Fix the poster of a portrait video coming out on its side – #1082
  • Fix isDecompressionEnabled and the delegate's decompression methods having no effect on macOS – #1089

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

Don't miss a new Nuke release

NewReleases is sending notifications on new releases.