Back to Nuke

Performance Guide

Documentation/Nuke.docc/Performance/performance-guide.md

13.0.511.5 KB
Original Source

Performance Guide

Learn about the performance features in Nuke and how to make the most of them.

Caching

Images can take a lot of space. By using Nuke, you can ensure that when you download an image, it will be cached so that you don't have to download it again. Nuke provides three different caching layers.

L1. Memory Cache (Default)

The images are stored in a fast in-memory cache: ImageCache. It uses LRU (least recently used) replacement algorithm and has a strict size limit. It also automatically evicts images on memory warnings and removes a portion of its contents when the application enters background mode.

Important: Nuke stores decompressed (bitmapped) images in the memory cache. If your app is loading and displaying high-resolution images, consider downsampling them and/or increasing cache limits. For context, a bitmap for a 6000x4000px image takes 92 MB (assuming it needs 4 bytes per pixel).

L2. HTTP Disk Cache (Default)

By default, unprocessed image data is stored in native URLCache, which is part of the Foundation URL Loading System. The main feature of URLCache is its support of Cache Control. Here is an example of an HTTP header with cache control.

HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
Expires: Mon, 26 Jan 2016 17:45:57 GMT
Last-Modified: Mon, 12 Jan 2016 17:45:57 GMT
ETag: "686897696a7c876b7e"

This response is cacheable, and will be fresh for 1 hour. When the response becomes stale, the client validates it by making a conditional request using the If-Modified-Since and/or If-None-Match headers. If the response is still fresh, the server returns status code 304 Not Modified to instruct the client to use cached data, or it would return 200 OK with new data otherwise.

Tip: Make sure that the images served by the server have Cache Control set correctly.

Important: By default, URLCache doesn't serve stale images offline. To show a stale image, pass the URLRequest with cache policy set to .returnCacheDataDontLoad and then perform a second request to refresh the image.

L3. Aggressive Disk Cache (Optional)

If your server uses unique URLs for images for which the contents never change, consider enabling DataCache (see ImagePipeline/Configuration-swift.struct/withDataCache that also takes care of disabling the default URLCache). It's a fast persistent cache with non-blocking writes that allows reads to be parallel to writes and each other. It also works offline and reduces pressure on URLSession.

Tip: By default, DataCache stores only the original image data. To also cache processed images, set a data cache policy that enables it. ImagePipeline/DataCachePolicy/automatic is a good default: it stores original data for unprocessed requests and processed images for requests with processors.

swift
ImagePipeline.shared = ImagePipeline(configuration: .withDataCache(dataCachePolicy: .automatic))

Tip: To save disk space, see ImageEncoders.ImageIO and ImageEncoder.isHEIFPreferred option for HEIF support.

Prefetching

Prefetching means downloading data ahead of time in anticipation of its use. It creates an illusion that the images are simply available the moment you want to see them – no networking involved. It's very effective. See doc:prefetching to learn more about how to enable it.

Important: If you apply processors when displaying final images, make sure to use the same processors for prefetching. Otherwise, Nuke will end up populating the memory cache with the versions of the images you are never going to need for display.

Decompression

Image formats often use compression to reduce the overall data size, but it comes at a cost. An image needs to be decompressed, or bitmapped, before it can be displayed. UIImage does not eagerly decompress this data until you display it. It leads to performance issues like scroll view stuttering. To avoid it, Nuke automatically decompresses the images in the background. Decompression only runs if needed; it won't run for already processed images.

Note: See Image and Graphics Best Practices to learn more about image decoding and downsampling.

Downsample Images

Ideally, the app should download the images optimized for the target device screen size, but it's not always possible. To reduce memory usage, downsample the images.

swift
// Target size is in points
let request = ImageRequest(url: url, processors: [.resize(width: 320)])

Tip: Some image formats, such as jpeg, can have thumbnails embedded in the original image data. If you are working with a large image and want to show only a thumbnail, consider using ImageRequest/ThumbnailOptions. If the thumbnails aren't available, they are generated. It can be up to 4x faster than using ImageProcessors/Resize for high-resolution images.

Main Thread Performance

Nuke has a range of optimizations across the board to ensure it does as little work on the main thread as possible.

  • CoW. The primary type in Nuke is ImageRequest. It has multiple options, so the struct is quite large. To make sure that passing it around is as efficient as possible, ImageRequest uses a Copy-on-Write technique.
  • OptionSet. In one of the recent versions of Nuke, ImageRequest was optimized even further by using option sets and reordering properties to take advantage of gaps in memory stride to reduce its memory layout.
  • ImageRequest.CacheKey. Most frameworks use strings to uniquely identify requests. But string manipulations are expensive, and this is why in Nuke, there is a special internal type, ImageRequest.CacheKey, which allows for efficient equality checks with no strings manipulation.

These are just some examples of the optimization techniques used in Nuke. There are many more. Every new feature in Nuke is designed with performance in mind to make sure there are no performance regressions ever.

Tip: One thing you can do to optimize the main thread's performance is create URLs in the background, as their initialization can be relatively expensive. It's best to do it during decoding.

Resumable Downloads

Make sure your server supports resumable downloads. If the data task is terminated when the image is partially loaded (either because of a failure or a cancellation), the next load will resume where the previous one left off. Resumable downloads require the server to support HTTP Range Requests. Nuke supports both validators: ETag and Last-Modified. Resumable downloads are enabled by default. You can learn more in "Resumable Downloads".

Coalescing

Thanks to coalescing (enabled by default), the pipeline avoids doing any duplicated work when loading images. Let's take the following two requests as an example.

swift
let url = URL(string: "https://example.com/image")

// Only one network request is made for both of these
pipeline.loadImage(with: ImageRequest(url: url, processors: [
    .resize(size: CGSize(width: 44, height: 44)),
    .gaussianBlur(radius: 8)
]))
pipeline.loadImage(with: ImageRequest(url: url, processors: [
    .resize(size: CGSize(width: 44, height: 44))
]))

Nuke will load the data only once, resize the image once and blur it also only once. There is no duplicated work done. When you request an image, the pipeline creates a dependency graph of tasks needed to deliver the final images and reuses the ones that it can.

Note: Coalescing is controlled by ImagePipeline/Configuration-swift.struct/isTaskCoalescingEnabled. It can be disabled if you need requests with the same URL to be treated as independent tasks.

Progressive Decoding

Nuke supports progressive JPEG, but it must be enabled in the pipeline configuration.

swift
ImagePipeline.shared = ImagePipeline {
    $0.isProgressiveDecodingEnabled = true
}

Once enabled, you’ll first see a blurry low-quality version of the full image, which gets sharper as more data arrives. Progressive previews are delivered through the same ImageTask progress handler or AsyncStream used for the final image.

Request Priorities

Nuke is fully asynchronous and performs well under stress. ImagePipeline distributes its work on operation queues dedicated to a specific type of work, such as processing and decoding. Each queue limits the number of concurrent tasks, respects the request priorities, and cancels the work as soon as possible.

Cancelling an ImageTask frees its associated network and CPU resources immediately. Thanks to coalescing, the underlying work is only cancelled when all requests sharing it have been cancelled — so cancelling one request doesn't affect others loading the same image.

Nuke allows you to set the request priority and update it for outstanding tasks. It uses priorities for prefetching: the requests created by the prefetcher all have .low priority to make sure they don't interfere with the "regular" requests. See doc:prefetching to learn more.

There are many other creative ways to use priorities. For example, when the user taps an image in a grid to open it full screen, you can lower the priority of the requests for the images that are not visible on the screen.

swift
final class ImageView: UIView {
    private var task: ImageTask?

    override func willMove(toWindow newWindow: UIWindow?) {
        super.willMove(toWindow: newWindow)

        task?.priority = newWindow == nil ? .low : .high
    }
}

Rate Limiting

If the app starts and cancels requests at a fast rate, Nuke will rate limit the requests, protecting URLSession. RateLimiter uses a classic token bucket algorithm. The implementation supports quick bursts of requests which can be executed without any delays when "the bucket is full". It is important to make sure RateLimiter only kicks in when needed, but when the user opens the screen, all the requests are fired immediately.

Auto Retry

Enable waitsForConnectivity on URLSession to indicate that the session should wait for connectivity to become available instead of failing the request immediately in case of a network failure.

Measure

If you want to see how the system behaves, how long each operation takes, and how many are performed in parallel, enable the ImagePipeline/Configuration-swift.struct/isSignpostLoggingEnabled option and use the os_signpost Instrument. For more information, see Apple Documentation: Logging and WWDC 2018: Measuring Performance Using Logging.

Selecting a System

Make sure you select one image loading framework and stick to it. If you use more than one framework, it will prevent them from managing the system resources efficiently, such as caches. If, for any reason, you must use more than one framework, ensure that they at least share the same memory and disk caches.