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, andLazyImageState/animatedImagetoNukeUI, andImageContainer/animationtoNuke– #958 ImageDecoders/Defaultparses every animated image it recognizes once, and attaches the result along with the encodedImageContainer/data.ImagePipeline/Configuration-swift.struct/isAnimatedImageParsingEnabledturns it off – #958- A decoder registered with
ImageDecoderRegistrycan attach anAnimatedImageSourceand anAnimatedImageFrameDecodingto play a format Image I/O can't read – #958 ImagePipeline/DataCachePolicy/storeEncodedImagesstores 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 whenImagePipeline/Configuration-swift.struct/isDiagnosticsEnabledis set – #960 - Add
ImageRequest/UserInfoKey/labelKey, a short name for the role of a request, recorded inImageTask/Metrics/label– #960 - Add
ImageDiagnosticsStringEnum, theCodableconformance the diagnostics enums share – #960 ImageRequest/Prioritynow conforms toCodable– #960- Add
ImagePipeline/Configuration-swift.struct/isDiagnosticsLoggingEnabled, which launching withNUKE_DIAGNOSTICS_LOGalso sets, andImageTask/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
ImageLoadertoNukeUI: it loads images intoUIImageViewandNSImageView, or into any object that draws itsImageLoader/State, and replacesNukeExtensions, 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-migrateandnuke-optimizeskills – #1048 - Add
llms.txt,llms-full.txt, andllms-api.txt, generated from the articles, the skills, and the sources, and included in every checkout at the version it resolved – #1048 - Add
AGENTS.mdandmake test-onefor 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:
ImagePipelineDelegatetypealias,ImageRequest.imageId,ImageRequest.UserInfoKey.imageIdKey,ImageRequest.UserInfoKey.scaleKey,ImageRequest.UserInfoKey.thumbnailKey,ImagePipeline.Configuration.maximumDecodedImageSize, andImageDecodingContext.maximumDecodedImageSize– #907 - Remove the soft-deprecated
userInfoparameter from theImageRequestinitializers; it's still available as a property – #909 ImageRequest.scaleandImageRequest.ThumbnailOptions.init(maxPixelSize:)now useCGFloat– #910- Remove Combine support:
ImagePipeline.imagePublisher(with:)andFetchImage.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/stateandImageTask/currentProgressare deprecated – #913 - Remove
ImageTask/Event/started, whichImageTask/eventsnever yielded, and addImagePipeline/Delegate/imageTaskDidStart(_:pipeline:)in its place – #914 ImageTasknow conforms toIdentifiable– #922DataCacheis nowSendableinstead of@unchecked Sendable.DataCache/flush()andDataCache/sweep()are nowasync, andDataCache/queueandflush(for:)are removed – #925- Add
AssetType/avifand detect theavifandavisbrands, whichImageContainer/typepreviously reported asnil– #926 DataLoader/Errornow conforms toSendable– #933ImageProcessors/CoreImageFilter/Erroris nowSendableinstead of@unchecked Sendable: its cases carry a filter name and descriptions instead of theCIFilter,CIImage, and[String: Any]payloads – #934ImageProcessors/CoreImageFilternow takes[String: any Sendable]filter parameters instead of[String: Any]– #934ImageLoadingOptionsand its nestedContentModes,TintColors, andTransitionnow conform toSendable, and theImageLoadingOptions/Transition/custom(_:)closure is now@MainActor @Sendable– #935ImageDecodingContext/init(request:data:isCompleted:urlResponse:cacheType:previewPolicy:isAnimatedImageParsingEnabled:)now takespreviewPolicyandisAnimatedImageParsingEnabled– #936ImageDecoders/Empty/assetTypeis now public – #940TaskQueue/maxConcurrentOperationCountand the matching initializer parameter are renamed tomaxConcurrentTaskCount; the old names are deprecated – #942ImagePipeline/Delegate/willCache(data:image:for:pipeline:)is nowasyncand returns the data to store instead of taking a completion closure. Returnnilto prevent caching – #943NukeUI,NukeExtensions, andNukeVideonow re-exportNuke, so importing them is enough. TheNukeUI.ImageRequesttypealias is removed – #946ImagePipeline/Delegatenow declares the isolation of every method instead of documenting it:willLoadData,willCache,imageTaskDidStart, andimageTask(_:didReceiveEvent:pipeline:)run onImagePipelineActor, and the rest arenonisolated– #945NukeUInow surfaces the typedImagePipeline/Errorinstead ofany Error:FetchImage/result,LazyImageState/result,LazyImageState/error, and theonCompletionandonFailurecallbacks – #949FetchImage/progressandLazyImageState/progressnow returnImageTask/Progress, whichFetchImagepublishes itself, instead of the nestedFetchImage.Progressobservable object that needed its own@ObservedObject. The old type is deprecated – #953- Add
ImagePipeline/Error/isCancelled– #952 Nuke_ImageDisplayingis renamed toImageDisplaying, is no longer@objc, and takes anImageContainer:nuke_display(image:data:)becomesnuke_display(_:). A conformance declared in an extension can no longer be overridden by a subclass – #958ImagePipeline/Configuration-swift.struct/imageDecodingQueuenow runs 2 concurrent tasks instead of 1, matchingimageDecompressingQueue. 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/isRateLimiterEnabledis deprecated and no longer does anything – #963 ImageTaskandImageDecoders/Videoare nowSendableinstead of@unchecked Sendable– #965- Add
TaskQueue/reservedTaskCount: the number of slots that the work with a priority lower than.normalcan't take – #981 - Replace the global
ImagePipeline/Configuration-swift.struct/isSignpostLoggingEnabledwith the per-pipelineImagePipeline/Configuration-swift.struct/signpostLog: signposts are sent while diagnostics are on, unless it's set tonil– #995 LazyImage/priority(_:)withnilnow keeps the request's own priority instead of resetting it to.normal– #1034LazyImage/processors(_:)withnilnow keeps the request's own processors; pass[]to remove them.nilinprocessors(_:)andpriority(_:)also takes back an earlier call – #1035NukeExtensionsnow depends onNukeUI– #1038LazyImageView/imageTaskis now read-only – #1039FetchImage/load(_:)andLazyImageView/requestkeep the running task when they load the same image again, instead of restarting the download – #1043ImagePipeline/ErrorandDataLoader/Errornow conform toLocalizedError:localizedDescriptionreturns theirdescription, a technical English message, instead of a generic one – #1048ImagePipeline/Errordescriptions now say what was received, such as atext/htmlpage sent with a200, 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:), andonSuccess, so the compiler error names the replacement – #1048 - Add
View/imagePipeline(_:)andEnvironmentValues/imagePipelineto set the pipeline of everyLazyImagein 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/dataLoadingQueueruns the requests below.normalpriority 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 – #981ImageCachehits are 5% faster – #975- Loading an image with
ImagePipeline/Configuration-swift.struct/dataCacheset makes 10 fewer allocations per download – #971 ImagePipeline/Erroris now 8 bytes instead of 97, which shrinksImageTask/Eventfrom 99 bytes to 26 – #967LazyImagechecks whether its request changed up to 5× faster on every body update when it keeps the same request – #969- Subscribing to
ImageTask/eventsno longer spawns aTask, which makes it 1.2–1.9× faster – #980 ImageCachehits for requests with processors are 1.5× faster – #972LazyImagechecks 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/eventsproducing 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
DataCacheconfiguration properties – #925 - Fix
DataCache.initreading and decoding its metadata file on the calling thread, typically the main thread during app launch – #925 - Fix
DataCacheupdating 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
DataCachesweeping only once per launch instead of everyDataCache/sweepInterval– #930 - Fix
DataCache/sweep()not recording the sweep date, so the next scheduled sweep ran again withinDataCache/sweepInterval– #932 - Fix
DataCachewriting entries non-atomically, so a read that arrived while the same key was being overwritten could return a truncated file – #937 - Fix
VideoPlayerViewnot resuming a looping video on macOS when the view is added back to a window – #951 - Fix
ImagePrefetcher/didCompletenot being called when stopping prefetching cancels the last outstanding request – #968 - Fix
ImageTask/events,ImageTask/progress, andImageTask/previewsmissing previews and progress updates sent before the subscription lands – #980 - Fix a cancelled download keeping its
ImagePipeline/Configuration-swift.struct/dataLoadingQueueslot when a customDataLoadingdoesn't callcompletionaftercancel()– #989 - Fix
ImagePipeline/data(for:)andImagePrefetcher/Destination/diskCachedownloading a thumbnail or processed request again when the original data is in the disk cache – #1004 - Fix
ImageRequest/Options/returnCacheDataDontLoadfailing requests with processors even when the original image is cached – #1007 - Fix an image stored in a full
ImageCachebeing 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
.automaticand.storeAllpolicies – #1016 - Fix
ImageRequest/Options/skipDataLoadingQueuebeing ignored when another request for the same resource is already waiting in the queue – #1005 - Fix a data race on
DataLoader/delegate– #998 - Fix
LazyImagerestarting a running request when its priority changes or when the view reappears after.lowerPriority, discarding what it had downloaded – #1034 - Fix setting
FetchImage/priorityorLazyImageView/priorityback tonilleaving the running task at the overridden priority – #1034 - Fix
DataCacheskipping the scheduled sweeps when the recorded last sweep date is in the future, e.g. after the device clock was corrected – #1024 - Fix
LazyImagestarting a new request with thepipeline,onStart, andonCompletionfrom when the view last appeared, and ignoring a change of the pipeline alone – #1037 - Fix
FetchImage/isLoadingremainingtruewhen the task passed toonStartis cancelled directly – #1039 - Fix
ImagePrefetcherskipping images that only have a progressive preview in the memory cache – #1022 - Fix
ImagePrefetcher/priorityupdates being applied out of order, leaving the outstanding prefetches at a stale priority – #1022 - Fix
ImageDecoders/Defaultpreviews ignoring the EXIF orientation, so a rotated progressive image snapped a quarter turn when the download completed – #1029 - Fix
ImageDecoders/Defaultdecoding full-size progressive previews for a request withImageRequest/thumbnailset – #1029 - Fix
ImageDecoders/Defaultnot attachingImageContainer/UserInfoKey/scanNumberKeyto the GIF preview, or to the final GIF that followed it – #1029 - Fix
ImageProcessors/GaussianBlurreturning a corrupted near-black image and allocating gigabytes of scratch memory for a radius of about1544or more – #1021 - Fix
ImageProcessors/GaussianBlurconverting wide-gamut (Display P3) images to device RGB – #1021 - Fix
ImageProcessors/Resize/init(width:unit:upscale:)andinit(height:unit:upscale:)capping the other dimension at9999, 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
LazyImagedocumentation – #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_tallocations withOSAllocatedUnfairLock– #908 TaskSubscriptionis now~Copyable, so the compiler rejects unsubscribing twice – #961- Fix the
cropdocumentation ofImageProcessors/Resizesaying the flag has no effect with.aspectFill; it has no effect with.aspectFit– #1021