ios之性能优化·优化app界面渲染与保持界面流畅性的技巧(代码片段)

Forever_wj Forever_wj     2022-12-05     242

关键词:

一、界面渲染流程

① 渲染流程分析
  • 计算机中的显示过程通常是通过 CPU、GPU、显示器协同工作来将图片显示到屏幕上,如下图所示:

在这里插入图片描述

  • 苹果为了解决图片撕裂的问题使用了 VSync + 双缓冲区的形式,就是显示器显示完成一帧的渲染的时候会向发送一个垂直信号 VSync,收到这个这个垂直信号之后显示器开始读取另外一个帧缓冲区中的数据而 App 接到垂直信号之后开始新一帧的渲染。
    • CPU 计算好显示内容,提交至 GPU;
    • GPU 经过渲染完成后将渲染的结果放入 FrameBuffer(帧缓存区);
    • 随后视频控制器会按照 VSync 信号逐行读取 FrameBuffer 的数据;
    • 经过可能的数模转换传递给显示器进行显示。
  • 最开始时,FrameBuffer 只有一个,这种情况下 FrameBuffer 的读取和刷新的效率问题会受到很大的影响,双缓冲机制就可以很好的解决这个问题:GPU 会预先渲染好一帧放入 FrameBuffer,让视频控制器读取,当下一帧渲染好后,GPU 会直接将视频控制器的指针指向第二个 FrameBuffer。
  • 双缓存机制虽然解决了效率问题,但是随之而言的是新的问题,当视频控制器还未读取完成时,例如屏幕内容刚显示一半,GPU 将新的一帧内容提交到 FrameBuffer,并将两个 FrameBuffer 而进行交换后,视频控制器就会将新的一帧数据的下半段显示到屏幕上,造成屏幕撕裂现象。
  • 为了解决这个问题,采用了垂直同步信号机制。当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和 FrameBuffer 更新,而目前 iOS 设备中采用的正是双缓存区 + VSync。
② 屏幕卡顿原因
  • 在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变,中间这个等待的过程就造成了掉帧,也就是会卡顿。
  • 如下图所示,是一个显示过程,第 1 帧在 VSync 到来前,处理完成,正常显示,第 2 帧在 VSync 到来后,仍在处理中,此时屏幕不刷新,依旧显示第 1 帧,此时就出现了掉帧情况,渲染时就会出现明显的卡顿现象:

在这里插入图片描述

二、卡顿检测

① FPS 监控
  • 苹果的 iPhone 推荐的刷新率是60Hz,也就是每秒中刷新屏幕 60 次,也就是每秒中有 60 帧渲染完成,差不多每帧渲染的时间是 1000/60 = 16.67 毫秒整个界面会比较流畅,一般刷新率低于 45Hz ,在 16.67ms 内没有准备好下一帧数据,就会出现明显的卡顿现象。
  • FPS 的监控可以通过 YYFPSLabel 来实现,该原理主要是依靠 CADisplayLink 来实现的,通过 CADisplayLink 来监听每次屏幕刷新并获取屏幕刷新的时间,借助link的时间差,来计算一次刷新刷新所需的时间,然后通过“刷新次数 / 时间差”得到刷新频次,然后使用次数(也就是1)除以每次刷新的时间间隔得到 FPS,并判断是否其范围,通过显示不同的文字颜色来表示卡顿严重程度,具体源码如下:
	#import "YYFPSLabel.h"
	#import "YYKit.h"
	
	#define kSize CGSizeMake(55, 20)
	
	@implementation YYFPSLabel 
	  CADisplayLink *_link;
	  NSUInteger _count;
	  NSTimeInterval _lastTime;
	  UIFont *_font;
	  UIFont *_subFont;
	
	  NSTimeInterval _llll;
	
	
	- (instancetype)initWithFrame:(CGRect)frame 
	  if (frame.size.width == 0 && frame.size.height == 0) 
	      frame.size = kSize;
	  
	  self = [super initWithFrame:frame];
	
	  self.layer.cornerRadius = 5;
	  self.clipsToBounds = YES;
	  self.textAlignment = NSTextAlignmentCenter;
	  self.userInteractionEnabled = NO;
	  self.backgroundColor = [UIColor colorWithWhite:0.000 alpha:0.700];
	
	  _font = [UIFont fontWithName:@"Menlo" size:14];
	  if (_font) 
	      _subFont = [UIFont fontWithName:@"Menlo" size:4];
	   else 
	      _font = [UIFont fontWithName:@"Courier" size:14];
	      _subFont = [UIFont fontWithName:@"Courier" size:4];
	  
	
	  //YYWeakProxy 这里使用了虚拟类来解决强引用问题
	  _link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
	  [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
	  return self;
	
	
	- (void)dealloc 
	  [_link invalidate];
	
	
	- (CGSize)sizeThatFits:(CGSize)size 
	  return kSize;
	
	
	- (void)tick:(CADisplayLink *)link 
	  if (_lastTime == 0) 
	      _lastTime = link.timestamp;
	      NSLog(@"sdf");
	      return;
	  
	
	  // 次数
	  _count++;
	  // 时间
	  NSTimeInterval delta = link.timestamp - _lastTime;
	  if (delta < 1) return;
	  _lastTime = link.timestamp;
	  float fps = _count / delta;
	  _count = 0;
	
	  CGFloat progress = fps / 60.0;
	  UIColor *color = [UIColor colorWithHue:0.27 * (progress - 0.2) saturation:1 brightness:0.9 alpha:1];
	
	  NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS",(int)round(fps)]];
	  [text setColor:color range:NSMakeRange(0, text.length - 3)];
	  [text setColor:[UIColor whiteColor] range:NSMakeRange(text.length - 3, 3)];
	  text.font = _font;
	  [text setFont:_subFont range:NSMakeRange(text.length - 4, 1)];
	
	  self.attributedText = text;
	
	
	@end
  • FPS 只用在开发阶段的辅助性的数值,因为它会频繁唤醒 runloop,如果 runloop 在闲置的状态被 CADisplayLink 唤醒则会消耗性能。
  • FPS 的监控,具体实现逻辑如下:
	class YDWFPSLabel: UILabel 
	
	    fileprivate var link: CADisplayLink = 
	        let link = CADisplayLink.init()
	        return link
	    ()
	    
	    fileprivate var count: Int = 0
	    fileprivate var lastTime: TimeInterval = 0.0
	    fileprivate var fpsColor: UIColor = 
	        return UIColor.green
	    ()
	    fileprivate var fps: Double = 0.0
	    
	    override init(frame: CGRect) 
	        var f = frame
	        if f.size == CGSize.zero 
	            f.size = CGSize(width: 80.0, height: 22.0)
	        
	        
	        super.init(frame: f)
	        
	        self.textColor = UIColor.white
	        self.textAlignment = .center
	        self.font = UIFont.init(name: "Menlo", size: 12)
	        self.backgroundColor = UIColor.lightGray
	        //通过虚拟类
	        link = CADisplayLink.init(target: YDWWeakProxy(target:self), selector: #selector(tick(_:)))
	        link.add(to: RunLoop.current, forMode: RunLoop.Mode.common)
	    
	    
	    required init?(coder: NSCoder) 
	        fatalError("init(coder:) has not been implemented")
	    
	    
	    deinit 
	        link.invalidate()
	    
	    
	    @objc func tick(_ link: CADisplayLink)
	        guard lastTime != 0 else 
	            lastTime = link.timestamp
	            return
	        
	        
	        count += 1
	        // 时间差
	        let detla = link.timestamp - lastTime
	        guard detla >= 1.0 else 
	            return
	        
	        
	        lastTime = link.timestamp
	        // 刷新次数 / 时间差 = 刷新频次
	        fps = Double(count) / detla
	        let fpsText = "\\(String.init(format: "%.2f", fps)) FPS"
	        count = 0
	        
	        let attrMStr = NSMutableAttributedString(attributedString: NSAttributedString(string: fpsText))
	        if fps > 55.0 
	            // 流畅
	            fpsColor = UIColor.green
	        else if (fps >= 50.0 && fps <= 55.0)
	            // 一般
	            fpsColor = UIColor.yellow
	        else
	            // 卡顿
	            fpsColor = UIColor.red
	        
	        
	        attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: fpsColor], range: NSMakeRange(0, attrMStr.length - 3))
	        attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: UIColor.white], range: NSMakeRange(attrMStr.length - 3, 3))
	        
	        DispatchQueue.main.async 
	            self.attributedText = attrMStr
	        
	    
	
② 通过 RunLoop 检测卡顿
  • 通过监听主线程 Runloop 一次循环的时间来判断是否卡顿,这里需要配合使用 GCD 的信号量来实现,设置初始化信号量为 0,然后开一个子线程等待信号量的触发,也是就是在子线程的方法里面调用 dispatch_semaphore_wait 方法设置等待时间是 1 秒,然后主线程的 Runloop 的 Observer 回调方法中发送信号也就是调用 dispatch_semaphore_signal 方法,此时时间可以置为 0 了,如果是等待时间超时则看此时的 Runloop 的状态是否是 kCFRunLoopBeforeSources 或者是 kCFRunLoopAfterWaiting,如果在这两个状态下两秒则说明有卡顿,详细代码如下:
	#import "YDWBlockMonitor.h"
	
	@interface YDWYDWlockMonitor ()
	  CFRunLoopActivity activity;
	
	
	@property (nonatomic, strong) dispatch_semaphore_t semaphore;
	@property (nonatomic, assign) NSUInteger timeoutCount;
	
	@end
	
	@implementation YDWBlockMonitor
	
	+ (instancetype)sharedInstance 
	  static id instance = nil;
	  static dispatch_once_t onceToken;
	
	  dispatch_once(&onceToken, ^
	      instance = [[self alloc] init];
	  );
	  return instance;
	
	
	- (void)start
	  [self registerObserver];
	  [self startMonitor];
	
	
	static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
	
	  YDWBlockMonitor *monitor = (__bridge YDWBlockMonitor *)info;
	  monitor->activity = activity;
	  // 发送信号
	  dispatch_semaphore_t semaphore = monitor->_semaphore;
	  dispatch_semaphore_signal(semaphore);
	
	
	- (void)registerObserver
	  CFRunLoopObserverContext context = 0,(__bridge void*)self,NULL,NULL;
	  // NSIntegerMax : 优先级最小
	  CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
	                                                          kCFRunLoopAllActivities,
	                                                          YES,
	                                                          NSIntegerMax,
	                                                          &CallBack,
	                                                          &context);
	  CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
	
	
	- (void)startMonitor
	  // 创建信号c
	  _semaphore = dispatch_semaphore_create(0);
	  // 在子线程监控时长
	  dispatch_async(dispatch_get_global_queue(0, 0), ^
	      while (YES)
	      
	          // 超时时间是 1 秒,没有等到信号量,st 就不等于 0, RunLoop 所有的任务
	          // 没有接收到信号底层会先对信号量进行减减操作,此时信号量就变成负数
	          // 所以开始进入等到,等达到了等待时间还没有收到信号则进行加加操作复原信号量
	          // 执行进入等待的方法dispatch_semaphore_wait会返回非0的数
	          // 收到信号的时候此时信号量是1  底层是减减操作,此时刚好等于0 所以直接返回0
	          long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
	          if (st != 0)
	          
	              if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)
	              
	                  //如果一直处于处理source0或者接受mach_port的状态则说明runloop的这次循环还没有完成
	                  if (++self->_timeoutCount < 2)
	                      NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
	                      continue;
	                  
	                  // 如果超过两秒则说明卡顿了
	                  // 一秒左右的衡量尺度 很大可能性连续来 避免大规模打印!
	                  NSLog(@"检测到超过两次连续卡顿");
	              
	          
	          self->_timeoutCount = 0;
	      
	  );
	
	@end
③ 微信 matrix
  • 此方案也是借助 runloop 实现的大体流程和方案三相同,不过微信加入了堆栈分析,能够定位到耗时的方法调用堆栈,所以需要准确的分析卡顿原因可以借助微信 matrix 来分析卡顿。当然也可以在方案2中使用 PLCrashReporter 这个开源的第三方库来获取堆栈信息。
  • 微信 matrix 的下载链接:微信 matrix
④ 滴滴 DoraemonKit
  • 实现方案大概就是在子线程中一直 ping 主线程,在主线程卡顿的情况下,会出现断在的无响应的表现,进而检测卡顿。
  • 滴滴 DoraemonKit 的下载链接:滴滴 DoraemonKit

三、 CPU 资源消耗优化

① 预排版
  • 预排版主要是对 CPU 进行减负。
  • 假设现在又个 TableView 其中需要根据每个 cell 的内容来定 cell 的高度。知道 TableView 有重用机制,如果复用池中有数据,即将滑入屏内的 cell 就会使用复用池内的 cell,做到节省资源,但是还是要根据新数据的内容来计算 cell 的高度,重新布局新 cell 中内容的布局,这样反复滑动 TableView 相同的 cell 就会反复计算其 frame,这样也给 CPU 带来了负担。如果在得到数据创建模型的时候就把 cell frame 算出,TableView 返回模型中的 frame 这样的话同样的一条 cell 就算来回反复滑动 TableView,计算 frame 这个操作也就仅仅只会执行一次,所以也就做到了减负的功能,如下图:一个 cell 的组成需要 modal 找到数据,也需要 layout 找到这个 cell 如何布局:

在这里插入图片描述

② 预解码 & 预渲染
  • 图片的渲染流程,在 CPU 阶段拿到图片的顶点数据和纹理之后会进行解码生产位图,然后传递到 GPU 进行渲染,主要流程图如下:

在这里插入图片描述

  • 如果图片很多很大的情况下解码工作就会占用主线程 RunLoop 导致其他工作无法执行比如滑动,这样就会造成卡顿现象,所以这里就可以将解码的工作放到异步线程中不占用主线程,可能有人会想只要将图片加载放到异步线程中在异步线程中生成一个 UIImage 或者是 CGImage,然后再主线程中设置给 UIImageView,此时可以写段代码使用 instruments 的 Time Profiler,查看一下堆栈信息:

在这里插入图片描述

  • 发现图片的编解码还是在主线程,针对这种问题常见的做法是在子线程中先将图片绘制到 CGBitmapContext,然后从 Bitmap 直接创建图片,例如 SDWebImage 三方框架中对图片编解码的处理,这就是 Image 的预解码,代码如下:
	dispatch_async(queue, ^
	 CGImageRef cgImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self]]].CGImage;
	 CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
	
	 BOOL hasAlpha = NO;
	 if (alphaInfo == kCGImageAlphaPremultipliedLast ||
	     alphaInfo == kCGImageAlphaPremultipliedFirst ||
	     alphaInfo == kCGImageAlphaLast ||
	     alphaInfo == kCGImageAlphaFirst) 
	     hasAlpha = YES;
	 
	
	 CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
	 bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
	
	 size_t width = CGImageGetWidth(cgImage);
	 size_t height = CGImageGetHeight(cgImage);
	
	 CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
	 CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
	 cgImage = CGBitmapContextCreateImage(context);
	
	 UIImage * image = [[UIImage imageWithCGImage:cgImage] cornerRadius:width * 0.5];
	 CGContextRelease(context);
	 CGImageRelease(cgImage);
	 completion(image);
	);
③ 按需加载
  • 顾名思义需要显示的加载出来,不需要显示的加载,例如 TableView 中的图片滑动的时候不加载,在滑动停止的时候加载(可以使用 Runloop,图片绘制设置 defaultModal 就行)。
④ 异步渲染
  • UIView 和 CALayer 的关系:
    • UIView 是基于 UIKit 框架的,能够接受点击事件,处理用户的触摸事件,并管理子视图;
    • CALayer 是基于 CoreAnimation,而 CoreAnimation 是基于 QuartzCode 的,所以 CALayer 只负责显示,不能处理用户的触摸事件;
    • UIView 是直接继承 UIResponder 的,CALayer 是继承 NSObject 的;
    • UIView 的主要职责是负责接收并响应事件;而 CALayer 的主要职责是负责显示 UI,UIView 依赖于 CALayer 得以显示。
  • UIView 主要负责时间处理,CALayer 主要是视图显示,异步渲染的原理其实也就是在子线程将所有的视图绘制成一张位图,然后回到主线程赋值给 layer 的 contents。例如 Graver 框架的异步渲染流程如下:

在这里插入图片描述

  • 核心源码如下:
	if (drawingFinished && targetDrawingCount == layer.drawingCount)
	
	  CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
	  
	      // 让 UIImage 进行内存管理
	      // 最终生成的位图  
	      UIImage *image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;
	      void (^finishBlock)(void) = ^
	          // 由于block可能在下一runloop执行,再进行一次检查
	          if (targetDrawingCount != layer.drawingCount)
	          
	              failedBlock();
	              return;
	          
	          //主线程中赋值完成显示
	          layer.contents = (id)image.CGImage;
	          // ...
	      
	      if (drawInBackground) dispatch_async(dispatch_get_main_queue(), finishBlock);
	      else finishBlock();
	  
	
	  // 一些清理工作: release CGImageRef, Image context ending
	
  • 当视图层次调整时,UIView、CALayer 之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。
⑤ 对象创建
  • 对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗 CPU 资源。尽量用轻量的对象代替重量的对象,可以对性能有所优化。比如 CALayer 比 UIView 要轻量许多,那么不需要响应触摸事件的控件,用 CALayer 显示会更加合适。如果对象不涉及 UI 操作,则尽量放到后台线程去创建,但可惜的是包含有 CALayer 的控件,都只能在主线程创建和操作。通过 Storyboard 创建视图对象时,其资源消耗

    ios底层原理:界面优化(代码片段)

    ...后回复 “进群” ,拉你进程序员交流群????????界面优化无非就是解决卡顿问,优化界面流畅度,以下就通过先分析卡顿的原因,然后再介绍具体的优化方案,来分析如何做界面优化界面渲染流程具体流程可... 查看详情

    android性能优化之gpu呈现模式分析

    参考技术A如图,如果显示的柱线较高,则说明绘制的时间越多,也是衡量一个app是否流畅的一个参考指标。在Android系统中是以每秒60帧为满帧的,那么只要将1秒÷60帧,就能得出每帧为16毫秒(ms)时为满帧的界限,每帧快于16ms... 查看详情

    android开发android界面性能优化

    ...根源3.4不合理的xml布局对绘制的影响3.5源码相关四.渲染性能4.1渲染性能概念4.2追踪渲染性能4.3渲染性 查看详情

    android性能优化之启动优化(代码片段)

    Android性能优化之启动优化1.启动窗口优化Android系统在Activity的窗口尚未启动完成前,会先显示一个启动窗口(StartingWindow),等界面的第一帧渲染完成后再从启动窗口切换到真正的界面显示,启动窗口通常情况... 查看详情

    android性能优化之启动优化(代码片段)

    Android性能优化之启动优化1.启动窗口优化Android系统在Activity的窗口尚未启动完成前,会先显示一个启动窗口(StartingWindow),等界面的第一帧渲染完成后再从启动窗口切换到真正的界面显示,启动窗口通常情况... 查看详情

    ios之性能优化·列表异步绘制

    一、前言iOS所提供的UIKit框架,其工作基本是在主线程上进行,界面绘制、用户输入响应交互等。当大量且频繁的绘制任务,以及各种业务逻辑同时放在主线程上完成时,便有可能造成界面卡顿、丢帧现象,即在16.7ms内未能完成1... 查看详情

    性能优化之快速响应的用户界面

    用于执行JavaScript和更新用户界面的进程通常被称为“浏览器UI线程”。JavaScript和用户界面更新在同一个进程中运行,因此一次只能处理一件事情。 ·任何JavaScript任务都不应当执行超过100毫秒,过长的运行时间导致... 查看详情

    ios性能优化-离屏渲染

    前言在使用UIKit的过程中,性能优化是永恒的话题。很多分析优化滑动性能的文章,只介绍了优化方法,却对背后的原理避而不谈,本文对其中原理进行了简单的总结!可以参考我之前写的一篇总结iOS性能优化... 查看详情

    react组件性能优化

    转自:https://segmentfault.com/a/1190000006100489React:一个用于构建用户界面的JAVASCRIPT库.React仅仅专注于UI层;它使用虚拟DOM技术,以保证它UI的高速渲染;它使用单向数据流,因此它数据绑定更加简单;那么它内部是如何保持简单高效的... 查看详情

    reactnative应用与优化

    一.这样设计的初衷是为了防止主线程被JavaScript代码执行阻塞,保持UI界面更流畅。JavaScript线程和主线程之间的交互是异步的。ReactNative概览二.ReactNative页面生命周期,初始化,加载数据,页面渲染,事件响应&... 查看详情

    reactnative应用与优化

    一.这样设计的初衷是为了防止主线程被JavaScript代码执行阻塞,保持UI界面更流畅。JavaScript线程和主线程之间的交互是异步的。ReactNative概览二.ReactNative页面生命周期,初始化,加载数据,页面渲染,事件响应&... 查看详情

    极光开发者沙龙之移动应用性能优化实践旧酒新瓶——换个角度提升app性能与质量

    旧酒新瓶——换个角度提升App性能与质量  主讲人:高亮亮 --- 饿了么移动技术部高级iOS工程师,负责饿了么商家版iOSAPP开发,对架构和系统底层有深入研究,擅长移动性能分析,troubleshooting,iOS逆向编程。  主讲时间... 查看详情

    setstate详解与react性能优化(代码片段)

    setState的同步和异步1.为什么使用setState开发中我们并不能直接通过修改state的值来让界面发生更新:因为我们修改了state之后,希望React根据最新的Stete来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化React并没有实... 查看详情

    ios开发性能优化大纲

    前言  今年团队工作计划的一部分是性能优化,主要会涉及到内存优化,启动优化,渲染优化,卡顿优化,耗电量,网络流量优化,弱网优化等。之前写过一篇内存优化策略的文章,大家感兴趣... 查看详情

    高性能滚动scroll及页面渲染优化

    最近在研究页面渲染及web动画的性能问题,以及拜读《CSSSECRET》(CSS揭秘)这本大作。本文主要想谈谈页面优化之滚动优化。主要内容包括了为何需要优化滚动事件,滚动与页面渲染的关系,节流与防抖,pointer-events:none优化滚... 查看详情

    性能优化思路概括

    参考技术A  对象的销毁和创建、对象属性的调整、布局计算、文本计算、文本排版、图片格式的转换和解码和图像的绘制等。  纹理的渲染;  Mac的缓存是分为前后帧缓存,可以增加处理速度。卡顿的... 查看详情

    前端性能高性能滚动scroll及页面渲染优化--转发

    本文主要想谈谈页面优化之滚动优化。主要内容包括了为何需要优化滚动事件,滚动与页面渲染的关系,节流与防抖,pointer-events:none优化滚动。因为本文涉及了很多很多基础,可以对照上面的知识点,选择性跳到相应地方阅读... 查看详情

    高性能滚动scroll及页面渲染优化

    本文主要想谈谈页面优化之滚动优化。主要内容包括了为何需要优化滚动事件,滚动与页面渲染的关系,节流与防抖,pointer-events:none优化滚动。因为本文涉及了很多很多基础,可以对照上面的知识点,选择性跳到相应地方阅读... 查看详情