Great performance is a prerequisite for delivering a compelling and immersive app experience that keeps users engaged. Learn best practices and strategies for characterizing and improving the performance of your code for iPhone, iPad, and Apple Watch.
I am an iOS Performance Engineer, and today we are going
to talk about "Performance on iOS and watchOS."
So we are going to start by telling you,
why should you think about performance?
If you've never thought about performance
in your app before, why start now?
Hopefully I'll convince you to stay in your seats,
and then we can move on to,
how should you think about performance?
It can be like a very broad and sometimes daunting topic,
but we are going to break it down into a couple of categories
and give you a few specific strategies
for improving the performance of your app in those areas.
And finally, you're hopefully really excited
to bring native code to watchOS 2, and we are going to dig
into what you can do to get the best user experience
in your app on that platform.
So why should you think about performance?
The easiest way to sum it up is that performance is a feature.
It's a core and central element
of giving your users a great experience in your app.
It's not an extra or a bonus or something you can get
to at the end if you have time.
It's actually something that should be
on your mind all the time while you are writing your app.
There's a couple of reasons for that.
If your app is really responsive,
if it always responds to user input right away,
that actually builds the trust of your users.
That lets them know that if they quickly need to access a piece
of information or service some interaction in your app,
it's not going to keep them waiting,
and that makes them really happy and keeps them coming back.
If you are going to adopt Multitasking on iPad in iOS 9,
not only does your app no longer have full run of the screen,
it actually doesn't have full run
of the system's resources either.
So a performance problem in your app doesn't just cause a bad
experience in your app anymore;
it can actually cause a bad experience
in another app as well.
You really want to be known as a good neighbor in Multitasking.
Apps that are architected to be efficient with system resources,
like CPU or memory, don't just feel great
when you are using them; they actually save battery power
and let your user get through their day,
and they really appreciate that.
And finally, iOS 9 supports a huge range of hardware,
and performance is a prerequisite of continuing
to bring great apps and features to all of your customers.
So hopefully I have convinced you, you don't have to walk out.
How should you think about performance?
Well, the very first step
when you are building your app is choosing technologies.
This is critical because you need to choose
which technologies are going
to give your users the best experience in your app.
Once you are a little bit of the way into building your app,
you can start taking measurements
and actually understanding what the user experience is
in the critical interactions in your app.
Your measurements will inform you how your app is doing today.
Once you've got those, you can actually set some goals
for what state you want your app to be
in before you submit it to the Store.
And finally, once you've got all that, you are ready
to start making code changes to improve the performance
in your app, and there's a great workflow you can follow
to converge on your targets.
So let's start with choosing technologies.
Picking the right tool for the job is a really early
and critical aspect
of proactively architecting your app for great performance.
And the very first step of choosing technologies is
to know the technologies.
So over the course of this talk,
I'm going to reference several other talks from this year
and previous years that cover technologies
that we have found really helpful
for getting great performance in apps.
Once you know the universe of technologies that are available
to you, then you can start to bring in your knowledge
of how your app works and what it needs to do,
and pick the best ones for your app.
A great example here is if your app needs
to store three strings, it's probably okay to write those
out to a plist or put them in user defaults.
On the other hand, if your app needs to use 3,000 strings,
you might want to consider Core Data.
Speaking of Core Data, when you are choosing technologies,
I strongly encourage you
to consider Apple APIs and frameworks.
We spent a lot of time trying to make those great for you
and your users, and we build our own products on them.
One benefit of adopting Apple APIs and frameworks is
that after the user installs your app,
they might install an iOS update,
and these frequently contain performance improvements
to core APIs and technologies.
So the next time they open your app
after installing an iOS update, it magically got faster.
So you've chosen some technologies,
and you've started building your app.
Let's measure a few things.
There's a couple of categories of performance
that we can think about measuring.
Let's start with animations.
Animations are what make your app feel alive and fluid
and let your user know where they are and what's going on.
The easiest way to measure the performance
of your animations is the Core Animation instrument.
Responsiveness is all about how quickly you react to user input,
and actually, the simplest way
of measuring responsiveness may seem a little bit low tech,
but it's really powerful,
and that is simply instrumenting your actual code.
I am going to go into an example of that.
For more complex scenarios that maybe involve several threads
or lots of system interactions,
there is a great instrument called System Trace.
And finally, memory.
Memory is the most precious resource on mobile devices,
and it's really important
to make sure your app is using exactly as much memory
as it needs and no more.
Again, a simple but powerful way
to understand your app's memory use --
which I am going to go into -- is the Xcode debugger.
When you are ready to dig in more,
there's a great instrument called Allocations.
And if you think you have leaks, there's an instrument
to help you track those down as well.
So let's go into a code instrumentation example.
Here I've got an IBAction that's wired to a button,
so when my user taps this, I am going to load an image
and put it in my view.
And I want to know how long this takes.
So I am going to use an API called 'CF absolute time
Now, I don't actually care about the absolute value
of the current time, but I do care
about the difference between these.
This API, although this is Swift and it's type inference
and it's wonderful, I will tell you this returns a double,
and specifically, that double represents the current time
A second is actually a really long time.
Your user is really going to feel it if something
in your app takes a second.
So we actually find milliseconds
to be a more actionable unit of measure here.
So we are going to subtract the start time from the end time
and multiply the result by a thousand
to get our measurement in milliseconds.
It's important to profile the release configuration
of your app so that you get all the compiler optimizations
that your users will get and so
that you understand how your app is actually behaving
in the field; however, it's equally important not
to submit your performance instrumentation
to the App Store.
So my suggestion is that you actually create a copy
of your release scheme in Xcode and define one additional define
so that you can build a release version of your app
with the performance instrumentation quickly
So what types of responsiveness are we interested in measuring?
Well, definitely taps and button presses.
Most commonly, you'll be doing these in IBActions.
You may also do them in UIView touch handling code.
Or you may have a gesture recognizer target.
Another aspect of your app's performance that's interesting
to understand is what it feels for the user to move
around in your app and switch to different views,
whether that's using a tab bar or modal views.
In this case, we find it interesting to think
about the time between 'view will appear'
and 'view did appear' because that lets you compare
which of your views take longer to get ready to get on screen.
So you've taken some measurements,
and you understand how your app is behaving.
How do you set goals for where you want your app
to be before you submit it to the Store?
Animations feel great, they feel realistic and fluid and alive
when they run at 60 frames per second.
I'm not going to talk too much about them this year
because there's a great talk from last year you can check
out called "Advanced Graphics and Animations in iOS Apps,"
and there they cover the Core Animation instrument
and how you can use it to measure the performance
of your animations and also improve it
across the full range of hardware.
We are going to spend a lot of time today on responsiveness.
Responsiveness is all about, again,
how you react to user input.
And we've found that if it takes too much longer
than a hundred milliseconds, your user starts to feel it.
So your goal for any responsiveness scenario should
be 100 milliseconds.
By the way, you want to think
about reaching these performance goals
on the oldest hardware you intend to support.
If you are targeting iOS 9, that might be the original iPad mini,
the iPhone 4s, or even the iPad 2.
If you've already got one of those --
or rather, if you've still got one of those --
please keep it, continue to use it, continue testing on it.
And if you don't, well, there's a great refurbished section
on the Apple Online Store.
So you've set goals.
You've taken measurements.
And now you want to proceed with making code changes in your app
to improve the performance.
How do we start?
First of all, don't guess.
You really would like to profile with tools and act on evidence
of what's causing the performance issues in your app.
It's charming to think that your intuition will be right all the
time, but it's probably closer to a coin toss.
Along those lines, avoid premature optimization.
Don't complexify your code until you have evidence
that the simplest possible implementation is not sufficient
for great performance.
Frequently, the mechanisms that people introduce to try
to proactively stave off performance problems end
up causing performance problems of their own.
Make one change at a time.
You do want to start to hone and develop your intuition for how
to approach improving the performance of your app,
and it's very hard to understand which thing you did
that actually ended up improving the performance of your app,
so make one change at a time.
And what I am really trying to say here is
that there's no magic.
This is just like ordinary debugging.
So bring the same rigorous, scientific mindset you would
to debugging an app crash or a functional issue.
So here is the picture I want you to print out,
stick on your wall, set as the wallpaper on your Mac.
This is how we go through making code changes
to improve performance in an app.
The first thing you need to do is reproduce the issue.
Then profile with tools to get an understanding of which areas
of your code are actually contributing
to a performance issue.
In a sufficiently large code base,
your intuition may actually not be correct here,
so it's good to collect evidence.
Then, once you've zoomed in on the specific sections of code
that are causing less-than-desirable performance
in your app, you can measure exactly how much time you are
And finally, you can make a single targeted code change
to try and advance towards your goals.
Often it's the case that one single change doesn't get you
all the way to where you want to be, and in fact,
several different changes will combine
to eventually get you to your goal.
So that's why this is a cycle because you may find
that after you change code and reproduce again, it's better,
but you are not where you want to be.
So you can keep going through the cycle until you are happy.
Profiling and measuring are on that slide, and maybe they seem
like they are similar, but actually,
they are two discrete steps
of improving the performance of your app.
Profiling, again, is using tools like the Xcode debugger
and Instruments: Time Profiler to get an overview
of all the aspects of your code that are contributing
to your performance scenario.
Measuring means instrumenting a specific area of your code
to understand precisely how much time the user spends
And again, the 'CF absolute time get current' example I gave is
actually a really powerful way to do that.
Again, for more complex scenarios, there's System Trace.
So let's talk about responsiveness.
Responsiveness is all about reacting to user input.
And we can't talk about responsiveness without talking
about your app's main thread
because your app's main thread is
where you consume all the user input.
That's anything from the touchscreen --
a tap or a scroll -- anything from the other sensors
on the device, like an orientation change,
and multitasking resizes and other system state events.
If your main thread is mostly dedicated to the task
of responding to user input,
your app will feel really great all the time.
If you're a little less careful with what you do
on your main thread or maybe you do everything
on your main thread,
it's possible your app will appear hung or frozen.
So what should we avoid doing on our main thread?
Specific things to watch out for include CPU-intensive work.
That can mean parsing a long string that you downloaded
from the network, maybe applying a filter to an image,
and tasks that depend on an external resource.
And I will come back to that.
I am not going to spend too much time on CPU-intensive work today
because there was a great talk earlier this week from the folks
that made Instruments called "Profiling in Depth,"
and they actually talk about improving the performance
of CPU-intensive code in Instruments using Instruments.
So let's go back to tasks that depend on an external resource.
Another name for these is a blocking call.
It's called that because it prevents your thread
from making progress, so you are blocked.
Now, what's a blocking call?
Some of you may be familiar with the concept of a syscall.
Any code path that ends
up making a syscall is considered a blocking call.
As I mentioned, these typically involve resources
that are not currently in memory, commonly things
like loading things from the disk
or fetching stuff over the network.
Occasionally, your main thread will also get blocked
because it's waiting for a resource that is available,
but it's waiting for someone else to finish using
that resource because that resource only allows one client
at a time.
So, how do you spot blocking calls in your thread?
Well, sometimes they jump right out at you.
The word "synchronous" is a synonym for blocking.
So that's a clue.
When you are reading through your code, that should light
up and draw your eyes.
So, great, we spotted this blocking call in my code,
'NSURLConnection send synchronous request.'
Well, what do I do now?
Well, sometimes there is an asynchronous API --
especially for APIs that specifically call
out that they're synchronous in their name --
that you can easily switch over to.
In this case, we got lucky, and in fact,
this one helpfully has the word "asynchronous" in the name,
so we know exactly what we are getting ourselves into.
Unfortunately, it's not quite as simple as search and replace.
You are changing the order that your code executes in,
and you may have other code that depends
on the result of this operation.
So unfortunately, some restructuring is required.
But let's say you don't have an async equivalent
that you can easily switch to or you want to move a whole section
of code off the main thread in one operation.
In that case, use Grand Central Dispatch.
Grand Central Dispatch is an Apple technology
that manages a global thread pool in your app.
It's already there.
Even if you don't notice it.
If you are familiar with programming with threads
on other platforms, Grand Central Dispatch sort of takes
out all the confusion and trouble of worrying
about starting threads and what state they are in and so on,
and lets you express tasks that you'd like to run
as closures or blocks.
These closures, once you submit them to Grand Central Dispatch,
run on an arbitrary thread in your process.
Arbitrary threads are awesome because you didn't have to deal
with starting them, and you don't have to think
about how many there are, but they have the caveat.
Since you don't control which thread your code is running on,
any operation you express in a closure
or block must be safe to do on any thread.
What are some examples?
Well, some objects are actually restricted to access
on the main thread only.
UIKit views and controllers, for example, must be created,
modified, and destroyed only on the main thread.
Some objects, like Foundation and Core Graphics objects,
allow use from any thread.
However, many of these have the additional stipulation
that the caller is responsible
for ensuring only one thread accesses them at the time.
They don't protect themselves internally.
So if you intend to use them for multiple threads,
you frequently have to implement the protection yourself,
and the preferred way of doing this is a serial GCD queue.
The best way to find out how your objects expect
to be treated is to read the headers.
Per object, usually near the initializer,
there should be a description of exactly how it expects
to be accessed by threads in your app.
So, let's go back to my example here.
What do I do in this code?
I load some data from a file.
I process and filter an image.
And finally, I put it in image view in my view hierarchy.
So right now when the user taps a button in my app,
my main thread looks a little bit like this.
It does those three things in order one after another.
Simple, straightforward, easy to understand, great.
Unfortunately, should the user happen to try and scroll
or rotate right when I am in the middle of doing this,
we won't be able to service that input until later.
And the thing with blocking calls is you never really know
how long they are going to take.
It's sort of like the weather.
So the user is going to be kept waiting for some unknown amount
of time, and that will make them sad.
So how do we fix this?
Well, we can use Grand Central Dispatch,
and we can use a Grand Central Dispatch API called
Now, 'dispatch async' takes two arguments.
The first thing you need to pass in is
which queue you want it to use.
As I mentioned, there's already several queues in your app
that GCD has created for you.
And I am going to get one
of them using the 'dispatch get global queue' API.
Because there are several, I have to actually tell GCD
which one I want, and here I am going
to use a 'Quality of Service' class.
'Quality of Service' is how you tell the system how important
the work you are asking it to do is relative to both other work
on your app and work across the rest of the system.
In this case, because this is the direct result
of a user action and the user is probably waiting for the result,
I am going to use the 'user-initiated' QOS class.
The final argument to 'dispatch async' is a closure containing
the code you would like it to run.
I am done.
It's off the main thread.
Right? Not quite.
As I mentioned, UIKit views and controllers are only safe
for use on the main thread, so I can't put them in this closure.
So that wasn't really the particularly slow part
of my code anyway, right?
The first two lines were the blocking calls.
Why don't I just move this back out onto the main thread?
Unfortunately, that's not quite going to work either
because I have actually changed the order
that this code executes in.
This closure is not guaranteed to have run
by the time 'dispatch async' returns.
Hopefully it will run quite shortly afterwards.
But most likely, once GCD has fired the work off
to the dispatch queue, it will immediately move
on to the next line, and at this point,
my image is still likely to be nil.
The user will never see their image.
That will also make them sad.
So how do we fix this?
Well, we can actually make another call to 'dispatch async,
and this time we'll use a very special queue called the
The main queue is guaranteed to be serviced by the main thread.
You can get it using the API dispatch 'get main queue.'
What that means is if you have objects that require access
on the main thread, you can still put them in a closure
and pass it to dispatch.
You just have to make sure they run on this queue.
So I have done that here and now my imageView is safe and happy.
With this, we've moved that work off the main thread,
but then when we've needed to use objects
that must be accessed on the main thread, we have done
so once the data is ready for them.
And by the way, the original problem we were trying to solve,
should the user try and scroll or rotate,
that will get serviced right away, and they will be happy.
So what types of blocking calls are you likely
to find in your code?
They are sneaky and they hide in all kinds of places.
As I mentioned, networking.
NSURLConnection and friends.
It's very easy to, without meaning to,
make a synchronous call to the network.
Usually you can switch to asynchronous API,
or if you actually want even more control
over when your application accesses the network --
and in some cases even let it download things
when it's not running --
I encourage you to check out the NSURLSession background session.
When you come across these in your code,
they don't look that scary.
It's one line.
How bad can it be?
But actually, some of these,
like ones with the name 'contents of file' or 'contents
of URL,' may end up having to use the disk or other resources
to service your request.
And finally, Core Data.
Again, they just look like objects, not bad, right?
Core Data frequently does a lot of I/O on your behalf.
Luckily, it's pretty easy to move some
of your heavy recorded operations
to different concurrency modes.
And actually, there's great new API in Core Data this year
to simplify that and other common bulk operations.
You can go learn about it in the session from the other day,
"What's New in Core Data."
So if you find a blocking call,
switch to asynchronous API or use GCD.
If you want to know more about GCD, including, again,
new API this year that's going to simplify common operations
and also the quality of service classes
that I mentioned earlier, there's a great talk
from just an hour ago called "Building Responsive
and Efficient Apps with GCD,"
and I encourage you to go watch it.
Let's move on to memory.
Like I said, memory is the most precious resource
on mobile devices.
If you are planning to adopt multitasking in your app, again,
you don't have full run of the screen anymore, so it stands
to reason that you also don't have full run of the rest
of the system's resources.
If you are going to bring some of the code from your app
to watchOS, it's especially important
that its memory footprint be compact.
Once again, iOS 9 supports a huge range of hardware.
If you want to bring your great apps and features to some
of the lower-end devices supported by the OS,
memory is extremely important on those systems.
And finally, if you are the developer of an extension,
think about the fact that your extension may now be called
on to run when there are two other apps on the screen.
So memory will be in high demand, and you need to be able
to get away with using as little as possible.
So let's get a little bit into how memory works on iOS.
There is not enough physical memory on any iOS device
to keep all the suspended apps in RAM at the same time.
When we come under memory pressure, we actually have
to evict things to make room for the foreground app.
On OS X or a PC operating system, we might write the state
of those apps out to disk first, but it turns
out that doesn't make sense on mobile devices,
so when you get evicted, you are just gone.
There's lots more detail here and lots to get into,
and there's actually a great talk from a few years ago called
"iOS App Performance Memory."
The slide template and colors are a little different,
but the information is really solid, so please go watch
that one if you are interested
to learn more about how this works.
But if you've never thought about memory in your app before,
it boils down to this -- reclaiming memory takes time.
If you've already used up all the available memory
in the system and then you need some more,
you might be kept waiting while the system evicts things
on your behalf.
If you suddenly request a large amount of memory,
the system might need to evict several different things
to service your request,
and that can influence the responsiveness of your app.
Conversely, when you are in the background,
if your footprint is very compact,
it's actually less likely that you will be one
of the things that gets evicted.
And so when the user then returns to your app,
you will be able to resume instead of launching,
and that will feel a lot faster.
So if you've never thought about memory before,
it's really important as a first step
to rationalize your app's memory footprint.
And what that means is to think about the types
of resources that it uses.
These might be strings; maybe long blobs of JSON or XML
that you downloaded from the network; images that, again,
came from the network or the user took them with the camera;
and again, Core Data managed objects, which use a lot
of underlying resources to sort of make the magic happen.
Once you've thought about these resources, you can start
to group them according to which user interactions depend
on each kind, and that helps you establish a mental model
of the resources that your app uses.
Once you've done that,
we can quickly check our work using the Xcode debugger.
To get into more detail, we would reach for the Allocations
and Leaks Instruments.
I am not going to go into those today, but please check
out this talk from last year called "Improving Your App
with Instruments" to get started with those.
So let's go back to the Xcode debugger.
I have downloaded the Photos framework example app
from the Apple Developer website.
I've plugged in my phone, opened the Xcode project,
and hit build and run.
And now I am just going to look at the top left
of my Xcode window at the debugger.
Zoom in on that.
So, here, right away I can see
without touching my phone the first number that's interesting
Now I know that right after my app launches,
before the user does anything,
I am using about 10 megabytes of memory.
The next data point that I want to collect is that I want to go
and do whatever the most common user interaction in my app is,
so because this is a Photos app, I am going to open a photo.
And now I've learned that to open a photo, my app needs
about another 2.5 megabytes of memory.
At this point, an additional interesting experiment is
to repeat the same action over and over.
So I might open the same photo or a couple
of different photos a few times.
If my memory footprint continues to grow,
I may have a memory issue worth investigating.
The final point of interest here is I am going
to use the Home button on my device to suspend the app,
and I am going to see what happens
when it goes into the background.
And it looks like it shrank down to actually a little bit smaller
than it was right after it launched.
This is a great balance to strike.
You don't want to repeat work that you did during launch
on resume, but you also want to stay compact in the background
to make sure your users actually get to experience that resume.
Important to note that the Photos framework example app
didn't actually have to do anything special or magical
to achieve that behavior.
It's actually just a really straightforward
and minimal implementation of Apple technologies,
and Apple technologies actually have this behavior often built
in, and they manage sort of their underlying resources
in response to application lifecycle events automatically.
So you don't need to worry about it.
However, if you have large objects or other resources
of your own that you'd like to sort of dynamically lose
and get back in response to application lifecycle events,
the easiest way to do it is to use NSCache.
In some cases, though, you might have things
that can't be neatly represented as evictable objects
for NSCache, and then you have
to actually implement custom code that responds
to system lifecycle notifications in your app.
A few that you might be interested
in are the 'did enter background' notification.
Your app will get this when it's suspended,
and this is what NSCache uses to actually shrink
when you go into the background.
Another interesting one is the memory warning notification.
The system actually sends this before it starts evicting
suspended apps to give them a chance to shrink,
and maybe they can avoid getting evicted
if their footprint goes down.
So here is a quick example of that.
I am going to use the default NSNotificationCenter.
I am going to add an observer for, in this case,
the 'did receive memory warning' notification.
And all I am going to do is call some 'custom cache
Maybe this walks a linked use of C structures
and frees some other memory.
An important note, if you do register
for NSNotificationCenter observer, especially in init,
please be sure to remove yourself on deinit.
So memory is actually so important
that there's another talk I am going
to encourage you to go watch.
It's called "Optimizing Your App
for Multitasking on iPad in iOS 9."
But actually, even if your app doesn't run on iPad,
or you have no plans to support Multitasking,
please go watch this talk.
They go into a lot of detail about the types of resources
that applications use,
the patterns they typically access them in,
and more information about how to have your app respond
to system memory state.
It's really great.
Last but not least, I hope you are all really excited
to bring native code to watchOS 2.
When you are thinking about how to build your watchOS 2 app,
you have to start with a great design,
a design that really focuses on the essential functions
of your app and makes them easy, delightful,
and accessible to the user.
If you need help with that,
there's a great session you can go watch called
"Designing for Apple Watch."
Once you've got a great design for your Apple Watch app,
then you can start to think about which aspects
of your iOS app it might make sense to reuse.
This could include actual code or familiar access patterns
to APIs and frameworks that are shared between the platforms.
Sometimes something you are doing
on iOS might actually not make sense on watchOS
for performance reasons.
And you will end up implementing new mechanisms
to achieve the same result on the other platform.
watchOS users expect short, simple interactions,
and they expect to always see recent and relevant data
in Apps, Notifications, and Glances.
What does this mean for you as an app developer?
The most likely thing the user will do
on watchOS is just launch the app and look
at the one thing it shows immediately afterwards.
So what are some things we can do to get great launch time
and great responsiveness on watchOS?
Focus on minimizing both the amount
of network traffic you generate and the amount of work you have
to do on the device to do something sensible with it.
If you are accessing a server that you can control
and add new APIs to, make sure you are sending appropriately
sized and formatted responses down to the Watch.
This can include something as simple as removing unused keys
from JSON or XML blobs; resizing images
so that the Watch can just display them exactly
as they came over the wire and doesn't have
to do any extra work; and if your API is accustomed
to feeding devices with large screens that can show 10
or 20 records at a time, it may be sending back all
that information in one call.
But actually, for the Watch,
you should send only the appropriate number
of records needed to display a single screen.
To show fresh, relevant information all the time,
it's important to use your iPhone app
to keep the app context updated.
The app context is a piece of bidirectional shared state
between the platforms.
So when the user takes an action on either end
that will cause them to expect to see something different
on the other end, that can be updated.
The API for doing this is 'watch connectivity update
A great time to do this is when your iPhone app gets woken
up by background app refresh.
When it's done downloading new information
and updating its own snapshot, it can also push
that information over to the Watch
so that it will be ready next time the user launches.
Finally, in case you're relying on a server
that you can't change for some reason,
let's say you're hitting a third-party API,
you can use the power of your iPhone's network connection
and CPU to actually implement an intermediary that formats
and sizes responses for the Watch.
The API you would use to do this is 'watch connectivity
So you would send a message to the iPhone,
requesting whatever you need,
and the iPhone would download it,
and do all the operations I mentioned, remove unused keys,
reduce the number of records, resize images.
Then it could send a compact and actionable reply to the Watch,
again, over WC Session.
So wrapping up, performance is a feature.
It's an essential aspect
of giving your users a great experience in your apps.
And it should be on your mind from day one
when you are building your apps.
Efficient apps feel great when you are using them,
they build your users' trust, and they save battery power.
Please go learn about all the Apple technologies I mentioned,
and when you are thinking
about building your app, choose the best ones.
Keep your main thread always ready for user input.
Understand when and why your app uses memory.
And to get a great experience on watchOS, download
and process a minimal set of information.
Here's some great written documentation you can get
into if you are starting to get interested in this stuff.
And again, here are the sessions that I mentioned.
The first few are about technologies
that we covered this year, and there's a few
from previous years as well.
Thanks, and have a great Friday.
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.