Adopting Efficient Networking Practices in iOS Apps

Methodologies to improve network efficiency

Luke Smith
Better Programming

--

Photo by Andrew Neel on Unsplash

Most apps rely on networking to communicate with a server to send and receive chunks of data.

While it’s easy to spin up a URLSession to retrieve data from a server and display it on the screen, it’s essential to consider how efficiently your app utilizes these sessions and requests.

In this article, we will look at ways to optimize requests, prioritise and, if necessary, delay triggering them. We’ll also discuss implementing appropriate timeouts and being mindful of user data when they are on a cellular network.

To help you make informed decisions on how to improve your app’s network stack and boost its performance, we’ll briefly review some high-level theories on how apps communicate with servers via HTTP.

By implementing these optimizations, you can significantly enhance your app’s responsiveness and provide a better user experience.

So, let’s dive in and explore how to improve your app’s networking capabilities!

Why you should care about efficient networking

Not everyone will use your app at home or in the office on a super-fast Wi-Fi connection. People use their phones everywhere, outside, on the train, on the bus, at the gym, etc.

As a developer, it’s crucial to consider these varying networking environments when developing your app. Even if users are on a fast network, responsiveness can still be impacted if you’re not careful.

You should especially consider network performance if your app uses any of these features:

  • Video conferencing & VoIP
  • Video & Audio streaming
  • Websocket updates
  • Uploading/Downloading files
  • Syncing large amounts of data with a backend service

Regardless, if your app does any type of networking, it’s worth exploring ways to do it more efficiently.

Introduction

Networking is expensive. Not only does it have a performance hit on battery because it’s constantly pinging the device's wireless radios, but it can also slow down the responsiveness of your app if you send a high volume of requests.

Let’s quickly recap how a client interacts with a server…

When we request data from a server, the client sends data packets, which actually don’t go directly to the server, as you might assume. They instead get added to the back of a queue containing other packets; this means the more requests you send, the longer the queue of packets to the server will be. This queuing increases the round-trip time between your app and the server, therefore, increasing your app's latency. But your server likely supports HTTP/2+, which uses multiplexing to help mitigate this problem.

Depending on the transport protocol your server supports, you will either use Network or URLSession, which is built upon Network but configured for loading HTTP and URL-based resources.

To keep this article concise, I will mainly discuss interacting with URLSession as you’re most likely using HTTP.

Adopting HTTP/3.0 & QUIC

For most of us, our server is probably using HTTP. If it is, we want to ensure we use the latest standardised version, if possible: HTTP/3.0 & QUIC.

In 2022 HTTP/3.0 had been made standard and is the first version to be built on top of QUIC. Let’s explore why QUIC is a great addition to HTTP…

QUIC is a general-purpose transport-layer protocol. It can be used with any compatible application-layer protocol, but HTTP/3 is its most frequent use case.

QUIC was created to replace the shortcomings of TCP. QUIC runs on top of UDP, which is responsible for physically delivering application data (e.g. an HTTP/3 message) between the client and server. UDP is a simple and lightweight protocol, which means that it’s fast, but on the other hand, it lacks many features essential for reliable and secure communication. QUIC implements these higher-level transport features, so the two protocols work together to optimize the delivery of HTTP data over the network. It also integrates most features of the TLS v1.3 security protocol and makes them compatible with its own delivery mechanism.

In short, QUIC has taken all the best qualities of TCP connections and TLS encryption and implemented them on UDP. This means that it’s extremely secure and requires only 1 roundtrip to establish a connection to the server.

Additionally, it features connection migration support — allowing connections to move seamlessly across different network interfaces without reestablishing a session, meaning no additional round trips are required.

From iOS 15, URLSession supports HTTP/3.0 & QUIC by default 🎉

If we know our server supports HTTP/3, we can construct our URLRequest assuming so:

You can easily check which protocol your server supports by inspecting the HTTP traffic via Xcode Instruments -> Network profile template.

TL;DR: With the HTTP/3 stack, you get better security, reduced round trips, and optimized round trip time. The result is a faster connection setup and lower latency. Which in turn gives you better responsiveness!

A Quick note on URLSession

URLSession objects are expensive, and we should try to minimise the amount we create. This is because they have a high memory footprint and take time to connect to the server.

If we send many requests within a short time frame, we should consider using a common URLSession instance to create data tasks. Why? URLSession uses a shared connection pool to share the same connection with a host. This means sending multiple requests can share that connection without establishing a new one. In turn, benefitting from fewer round trips.

It’s worth noting URLSession connections are alive for only a brief time (~20 secs), and then the session will close them, so it will create a new connection each time for non-frequent requests.

Multipath TCP

Users often move in and out of range from Wi-Fi, switching between cellular networks and back again. With Multipath TCP enabled, URLSession initiates the request on both cellular and Wi-Fi and selects the more responsive of the two with a preference for Wi-Fi. This saves multiple round-trips when switching networks, reducing latency.

Enabling Multipath TCP:

  1. Enable the Multipath Entitlement Xcode capability.
  2. Set the multipathServiceType property on URLSessionConfiguration.

Note: This requires our server to have Multipath TCP enabled.

Configure the appropriate service type

The network service type hints to the operating system about what the purpose of our request is. This enhances the system’s ability to prioritize traffic, determine how quickly it needs to wake up the cellular or Wi-Fi radio, and so on. By setting an appropriate value, we optimize the device's battery life and performance.

For example, specify the .background type if your app is performing a download that wasn’t requested by the user, like prefetching content so that it’s available when the user chooses to view it. Or consider .responsiveData for requests the user is actively waiting on, such as when they are sending a payment request.

Handling large data transfers

What about requests that are exceptionally large, such as file transfers or when we transmit large data chunks?

If these tasks aren’t urgent, we can perform these when the app is inactive.

It’s important we classify requests appropriately so the system can manage traffic efficiently and reduce delays hence making foreground traffic faster.

Triggering large data transfers without consideration will affect the shared networking environment by creating a longer buffer queue.

If we spin up a user-initiated task, such as showing a profile screen, that request would get delayed because our server is congested from still processing packets from our large data transfer, which results in higher latency and affects the responsiveness. I.e. the user will see that loading indicator for longer than what’s necessary.

Here’s how we can configure a background URLSession using URLSessionConfiguration.background and setting isDiscretionary to true for optimal performance:

We can receive and respond to background tasks from the AppDelegate and URLSessionDelegate, respectively.

You can read more about creating background sessions here.

Be considerate of your user's data

Most users will be on a data plan and be conservative about how much data they use when they’re off Wi-Fi. Your app should respect this and honour Low data mode if enabled. In addition, for newer devices that support 5G, there’s Smart data mode which is turned on by default.

For example, we might be downloading a large high res image. This would use a lot of mobile data if done repeatedly. We should instead provide an alternative request that’s low data mode compliant. Such as downloading a low res image instead. We can achieve this by setting allowsConstrainedNetworkAccess to false on URLRequest.

Here’s an example:

In addition, consider setting waitsForConnectivity to true for delaying unurgent, long, and expensive networking tasks until the user is on a stable Wi-Fi connection. This way, the request will automatically re-trigger when the networking conditions are ideal:

Users can see how much data your app uses by going to Settings -> Mobile Data. If your app uses a lot of data and isn’t low data mode compliant, it could result in user churn.

Prioritise URLSessionDataTasks

We can hint to our server a priority at which it should handle a certain URLSessionTask from our app. It’s important to note however that setting a priority only provides a hint and doesn’t guarantee a performance benefit.

Here’s how you would set a priority:

There are 3 priorities we can choose from:

  • URLSessionTask.highPriority
  • URLSessionTask.defaultPriority <- The default value
  • URLSessionTask.lowPriority

Timeouts

It’s possible a user could be on a ‘patchy’ network, for example, if they are on a train and the connection is constantly dropping and picking up again.

This causes our requests to be temporarily idle.

If we send a request that remains idle for longer than the timeout interval, the request will fail. The default timeout interval is 60 seconds.

We should consider setting custom timeouts on long-running requests to avoid timing out.

Apple recommends only triggering a timeout when we stop making progress with a request, not when progress is taking too long. If the user is on a 3G network, they might be happy to wait longer for that data transfer to happen rather than not at all.

There are 2 different types of timeouts we can consider on URLSessionConfiguration:

Testing

Finally, don’t forget to use the Network Link Conditioner to simulate various networking environments on your app, test it with, Cellular, 3G, LTE, and custom configure specific conditions.

You can enable this tool by going to Settings -> Developer -> Network Link Conditioner.

You can also look at implementing the URLSessionTaskDelegate method didFinishCollectionMetrics to inspect metrics for each request-and-response transaction made during the execution of a task. It’s useful for measuring latency and round trips made to the server.

Conclusion

Networking is a huge topic, and I’ve only touched the surface. There are many additional things you should consider when being efficient with networking, such as handling rate-limits, caching, security, authentication, and reducing request size. Overall the optimisations you make will depend entirely on the app you have. And it’s crucial you understand what environments your users are in when they interact with your app in the real world.

I hope this article encourages you to explore further what optimisations you can make!

If you have any questions or think I missed something, please leave some feedback below! Thanks for reading!

--

--