1. Cache your data
One of the most common optimizations are to ensure that expensive calls are cached. Examples of these are GetComponent or FindObjectWithTag:
A maybe less obvious caching is that of WaitForSeconds used in coroutines:
Debug.Log has a lot of overhead and it is very important all of them are removed when no longer needed. To be on the safe side that there are no Debug.Log calls in a release build you can add the code below to one of your scripts:
This is not an optimizations tip but more of a heads up. It might be surprising but using strings can build up a lot of garbage collection. As strings are immutable, manipulations to a string will result in memory allocation for a whole new string. Bottom line, do not add string manipulations to your Update method.
All objects under a canvas is handled as one atlas so if one object is changed all other objects under that canvas also needs to be redrawn. To improve performance split up the different areas of the UI as several different canvases. To keep the hierarchy tidy just add all canvases as children of an empty game objects
Pixel Perfect is a setting which forces UI elements to be drawn with direct alignment to the pixels on the screen which often makes the UI look sharper. This however does not work well with scroll rects and has a huge performance impact so ensure that the canvas that holds the scroll rect has this feature disabled