preface
Recently, the company’s project needs to optimize the H5 page in seconds, so we investigated the plan on the market, and combined with the company’s specific business needs to do an optimization practice in this aspect. This article is a record of this optimization practice, and the source code is attached to download at the end of the article.
Look at the effect
Optimization idea
First, let’s look at the steps to load an H5 page on iOS platform:
Initialize webView -> request page -> Download data -> parse HTML -> request JS/CSS resources -> DOM render -> parse JS execution -> JS request data -> parse render -> download render image
Since all the pages seen by users before DOM rendering are white screens, the optimization idea is to analyze the time spent in each step before DOM rendering and optimize the part with the highest cost performance. This can be divided into the front-end can do optimization, and the client can do optimization, the front-end needs to cooperate with the front-end side, not discussed in this article, this article mainly discusses the client can do optimization ideas. The general idea goes something like this:
- Can cache as far as possible cache, with space for time. Here can go to intercept all resource requests of the H5 page, including HTML, CSS/JS, pictures, data, etc., the right client to take over the cache strategy of resources (including the maximum space occupied by the cache, the elimination of the cache algorithm, cache expiration strategy);
- What can be preloaded, preloaded in advance. It can deal with some time-consuming operations in advance, such as initialization of webView waiting to be used in advance when the App starts;
- Where it can be done in parallel, it will be done in parallel, taking advantage of the multi-core capabilities of the equipment. For example, when loading webView, you can load the resources needed at the same time;
Initialize the WebView phase
Loading a web page on the client is different from loading a web page on the PC. On the PC, you directly enter a URL in the browser to start the connection. On the client, you need to start the browser kernel first, initialize some global services and resources of the WebView, and then start the connection. Take a look at how long this phase of Meituan testing took:
When the client opens the H5 page for the first time, there will be a webView initialization time,
You can see that with WKWebView, the first initialization takes more than 760 milliseconds, so if you can load the web page with the already initialized WebView, then this part of the time is not needed.
Here we implement a webView buffer pool solution, which is initialized when the App starts. When we need to open a web page, we can directly fetch the Webview from the buffer pool:
+ (void)load
{
[WebViewReusePool swiftyLoad];
}
@objc public static func swiftyLoad(a) {
NotificationCenter.default.addObserver(self, selector: #selector(didFinishLaunchingNotification), name: UIApplication.didFinishLaunchingNotification, object: nil)}@objc static func didFinishLaunchingNotification(a) {
// Preinitialize the WebView
WebViewReusePool.shared.prepareWebView()
}
func prepareWebView(a) {
DispatchQueue.main.async {
let webView = ReuseWebView(frame: CGRect.zero, configuration: self.defaultConfigeration)
self.reusableWebViewSet.insert(webView)
}
}
Copy the code
Establish a connection -> DOM pre-render stage
# request blocking
SetURLSchemeHandler method provided by WKWebView can be used to add a custom Scheme on iOS 11 and above. Compared with the Scheme of NSURLProtocol private API, there is no audit risk. Then you can intercept all custom requests in the WKURLSchemeHandler protocol:
// Custom interception request starts
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
let headers = urlSchemeTask.request.allHTTPHeaderFields
guard letaccept = headers? ["Accept"] else { return }
guard letrequestUrlString = urlSchemeTask.request.url? .absoluteStringelse { return }
if accept.count> ="text".count && accept.contains("text/html") {
/ / HTML to intercept
print("html = \(String(describing: requestUrlString))")
// Load local cached resources
loadLocalFile(fileName: creatCacheKey(urlSchemeTask: urlSchemeTask), urlSchemeTask: urlSchemeTask)
} else if (requestUrlString.isJSOrCSSFile()) {
/ / js | | the CSS file
print("js || css = \(String(describing: requestUrlString))")
loadLocalFile(fileName: creatCacheKey(urlSchemeTask: urlSchemeTask), urlSchemeTask: urlSchemeTask)
} else if accept.count> ="image".count && accept.contains("image") {
/ / picture
print("image = \(String(describing: requestUrlString))")
guard letoriginUrlString = urlSchemeTask.request.url? .absoluteString.replacingOccurrences(of:"customscheme", with: "https") else { return }
// Images can use the cache strategy provided by SDWebImageManager
SDWebImageManager.shared.loadImage(with: URL(string: originUrlString), options: SDWebImageOptions.retryFailed, progress: nil) { (image, data, error, type, _._) in
if let image = image {
guard let imageData = image.jpegData(compressionQuality: 1) else { return }
// Resend the request if the resource does not exist
self.resendRequset(urlSchemeTask: urlSchemeTask, mineType: "image/jpeg", requestData: imageData)
} else {
self.loadLocalFile(fileName: self.creatCacheKey(urlSchemeTask: urlSchemeTask), urlSchemeTask: urlSchemeTask)
}
}
} else {
// other resources
print("other resources = \(String(describing: requestUrlString))")
guard let cacheKey = self.creatCacheKey(urlSchemeTask: urlSchemeTask) else { return }
requestRomote(fileName: cacheKey, urlSchemeTask: urlSchemeTask)
}
}
/// call when the custom request ends
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask){}Copy the code
Implement resource caching
Here using SWIFT to achieve memory and disk cache logic, main reference (Chao) test (XI) YYCache ideas and source code, memory cache using double linked list (logic) + hashMap (storage) LRU cache elimination algorithm, add, delete, change and check are O(1) time complexity, Disk caching uses sandbox file storage. The two kinds of cache realize the cache management of three dimensions: cache duration, cache quantity and cache size.
The way the protocol is used defines the interface API:
protocol Cacheable {
associatedtype ObjectType
/// Total number of caches
var totalCount: UInt { get }
/// Total cache size
var totalCost: UInt { get }
/// Whether the cache exists
///
/// -parameter key: cache key
/// - Returns: results
func contain(forKey key: AnyHashable) -> Bool
// return the cache for the specified key
///
/// - Parameter key:
/// - Returns:
func object(forKey key: AnyHashable) -> ObjectType?
// Set cache k and v
///
/// - Parameters:
/// - object:
/// - key:
func setObject(_ object: ObjectType, forKey key: AnyHashable)
// Set cache k, v, and C
///
/// - Parameters:
/// - object:
/// - key:
/// - cost:
func setObject(_ object: ObjectType, forKey key: AnyHashable, withCost cost: UInt)
// delete the cache for the specified key
///
/// - Parameter key:
func removeObject(forKey key: AnyHashable)
// delete all caches
func removeAllObject(a)
// clean up according to the cache size
///
/// -parameter cost: cache size
func trim(withCost cost: UInt)
// clean up according to the number of caches
///
/// -parameter count: indicates the number of caches
func trim(withCount count: UInt)
// Clean up according to the cache duration
///
/// -parameter age: cache duration
func trim(withAge age: TimeInterval)
}
extension Cacheable {
func setObject(_ object: ObjectType, forKey key: AnyHashable) {
setObject(object, forKey: key, withCost: 0)}}Copy the code
Usage:
/// h5 page resource cache
class H5ResourceCache: NSObject {
// memory cache size: 10M
private let kMemoryCacheCostLimit: UInt = 10 * 1024 * 1024
/// Disk file cache size: 10M
private let kDiskCacheCostLimit: UInt = 10 * 1024 * 1024
/// Disk file cache duration: 30 minutes
private let kDiskCacheAgeLimit: TimeInterval = 30 * 60
private var memoryCache: MemoryCache
private var diskCache: DiskFileCache
override init() {
memoryCache = MemoryCache.shared
memoryCache.costLimit = kMemoryCacheCostLimit
diskCache = DiskFileCache(cacheDirectoryName: "H5ResourceCache")
diskCache.costLimit = kDiskCacheCostLimit
diskCache.ageLimit = kDiskCacheAgeLimit
super.init()}func contain(forKey key: String) -> Bool {
return memoryCache.contain(forKey: key) || diskCache.contain(forKey: key)
}
func setData(data: Data, forKey key: String) {
guard let dataString = String(data: data, encoding: .utf8) else { return }
memoryCache.setObject(dataString.data(using: .utf8) as Any, forKey: key, withCost: UInt(data.count)) diskCache.setObject(dataString.data(using: .utf8)! , forKey: key, withCost:UInt(data.count))}func data(forKey key: String) -> Data? {
if let data = memoryCache.object(forKey: key) {
print("This is the memory cache.")
return data as? Data
} else {
guard let data = diskCache.object(forKey: key) else { return nil}
memoryCache.setObject(data, forKey: key, withCost: UInt(data.count))
print("This is disk cache.")
return data
}
}
func removeData(forKey key: String) {
memoryCache.removeObject(forKey: key)
diskCache.removeObject(forKey: key)
}
func removeAll(a) {
memoryCache.removeAllObject()
diskCache.removeAllObject()
}
}
Copy the code
The effect
Matters needing attention
#1. The crash occurs when the network load callback is still called after the WKURLSchemeHandler object instance is releasedThe task has already been stopped
The error
Solution: Use a dictionary to hold the state of the WKURLSchemeTask instance at the beginning and end of the interception request
// MARK:- Request interception begins
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
holdUrlSchemeTasks[urlSchemeTask.description] = true
}
/// call when the custom request ends
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
holdUrlSchemeTasks[urlSchemeTask.description] = false
}
// Add a layer of judgment where you need to use urlSchemeTask instances
// Whether urlSchemeTask ends prematurely, after which the instance method will crash
if let isValid = self.holdUrlSchemeTasks[urlSchemeTask.description] {
if! isValid {return}}Copy the code
#2. Garbled web pages
Add network request response receiving format:
manager.responseSerializer.acceptableContentTypes = Set(arrayLiteral: "text/html"."application/json"."text/json"."text/javascript"."text/plain"."application/javascript"."text/css"."image/svg+xml"."application/font-woff2"."application/octet-stream")
Copy the code
# 3. WKWebView hang
/ / white
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if webview.title == nil {
webview.reload()
}
}
/ / white
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
webView.reload()
}
Copy the code
The source code
Github.com/ljchen1129/…
TODOList
- Writing unit tests
- Remove third-party libraries SDWebImage and AFNetworking and use native implementations
- Resource preloading logic
- Unified exception management
- More Swift style
The resources
- blog.cnbang.net/tech/3477/
- Mp.weixin.qq.com/s/0OR4HJQSD…
- Juejin. Cn/post / 684490…
- Tech.meituan.com/2017/06/09/…
If you are interested, you can follow my public account “Qingzheng Elder Brother”.