这是因为我们的项目往往也是阶段性的:快速功能开发 -> 出现性能问题 -> 优化性能 -> 快速功能开发。
建立一个完善的性能指标体系,便可以在需求开发阶段发现页面性能的下降,及时进行修复。
为什么需要进行性能优化呢?这是因为一个快速响应的网页可以有效降低用户访问的跳出率,提升网页的留存率,从而收获更多的用户。参考《经济时报》如何超越核心网页指标阈值,并使跳出率总体提高了 43%,这个例子中主要优化了两个指标:Largest Contentful Paint (LCP) 和 Cumulative Layout Shift (CLS)。
除此之外,页面速度是一个重要的搜索引擎排名因素,它影响到你的网页是否能被更多用户访问。
我们来看下常见的前端性能指标,由于网页的响应速度往往包含很多方面(页面内容出现、用户可操作、流畅度等等),因此性能数据也由不同角度的指标组成:
这些是 User-centric performance metrics 中介绍到的指标,其中 FCP、LCP、FID、INP/TTI 在我们常见的前端开发中会比较经常用到。
最简单的,一般前端应用都会关心以下几个指标:
除了这些简单的指标外,我们要如何建立起对网页完整的性能指标呢?一套成熟又完善的解决方案为 Google 的 PageSpeed Insights (PSI) 。
PageSpeed Insights (PSI) 是一项免费的 Google 服务,可报告网页在移动设备和桌面设备上的用户体验,并提供关于如何改进网页的建议。
前面在《补齐Web前端性能分析的工具盲点》一文中,我们简单介绍过 Google 的另外一个服务–Lighthouse。
PageSpeed Insights 和 Lighthouse 的区别主要为:
特征 | PageSpeed Insights | Lighthouse |
---|---|---|
如何访问 | https://pagespeed.web.dev/(浏览器访问;无需登录) | Google Chrome 浏览器扩展(推荐非开发人员使用) Chrome DevTools Node CLI 工具 Lighthouse CI |
数据来源 | Chrome 用户体验报告(真实数据) Lighthouse API(模拟实验室数据) | Lighthouse API |
评估 | 一次一页 | 一次一页或一次多页 |
指标 | 核心网络生命、页面速度性能指标(首次内容绘制、速度指数、最大内容绘制、交互时间、总阻塞时间、累积布局偏移) | 性能(包括页面速度指标)、可访问性、最佳实践、SEO、渐进式 Web 应用程序(如果适用) |
建议 | 标有Opportunities and Diagnostics 的部分提供了提高页面速度的具体建议。 | 标有Opportunities and Diagnostics 的部分提供了提高页面速度的具体建议。堆栈包可用于定制改进建议。 |
简单来说,PageSpeed Insights 可同时获取实验室性能数据和用户实测数据,而 Lighthouse 则可获取实验室性能数据以及网页整体优化建议(包括但不限于性能建议)。
我们之前提到过,前端性能监控包括两种方式:合成监控(Synthetic Monitoring,SYN)、真实用户监控(Real User Monitoring,RUM)。这两种监控的性能数据,便是分别对应着实验室数据和用户实测数据。
实测数据是通过监控访问网页的所有用户,并针对其中每个用户的各自的体验,衡量一组给定的性能指标来确定的。和实验室数据不同,由于现场数据基于真实用户访问数据,因此它反映了用户的实际设备、网络条件和用户的地理位置。
当然,实测数据也可以由用户真实访问页面时进行上报收集,稍微大一点的前端应用都会这么做。但在此之前,如果你的前端网页没有做数据上报监控,也可以使用 PageSpeed Insights 工具进行简单的测试。但考虑到 PageSpeed Insights 收集的用户皆基于 Chrome 浏览器(CrUX),且需要登录的应用无法有效地获取真实数据,那么自行搭建一套性能指标体系则是最好的。
虽然实际上 PageSpeed Insights 服务并不能解决我们所有的问题,但是我们可以参考它的性能指标,来搭建自己的性能体系呀。
参考 Google 的 PageSpeed Insights,我们知道 PSI 会报告真实用户在上一个 28 天收集期内的 First Contentful Paint (FCP)、First Input Delay (FID)、Largest Contentful Paint (LCP)、Cumulative Layout Shift (CLS) 和 Interaction to Next Paint (INP) 体验,同时 PSI 还报告了实验性指标首字节时间 (TTFB) 的体验。
其中,核心网页指标包括 FID/INP、LCP 和 CLS。
First Input Delay (FID) 衡量的是从用户首次与网页互动(即,点击链接、点按按钮或使用由 JavaScript 提供支持的自定义控件)到浏览器能够实际开始处理事件处理脚本以响应该互动的时间。
我们可以使用 Event Timing API 在 JavaScript 中衡量 FID:
1 | new PerformanceObserver((entryList) => { |
实际上,从 2024 年 3 月开始,FID 将替换为 Interaction to Next Paint (INP),后面我们会着重介绍。
Largest Contentful Paint (LCP) 指标会报告视口内可见的最大图片或文本块的呈现时间(相对于用户首次导航到页面的时间)。
我们可以使用 Largest Contentful Paint API 在 JavaScript 中测量 LCP:
1 | new PerformanceObserver((entryList) => { |
许多网站都面临布局不稳定的问题:DOM 元素由于内容异步加载而发生移动。
Cumulative Layout Shift (CLS) 指标便是用来衡量在网页的整个生命周期内发生的每次意外布局偏移的最大突发布局偏移分数。我们可以从Layout Instability
方法中获得布局偏移:
1 | addEventListener("load", () => { |
布局偏移分数是该移动两个测量的乘积:影响比例和距离比例。
1 | layout shift score = impact fraction * distance fraction |
FID 仅在用户首次与网页互动时报告响应情况。尽管第一印象很重要,但首次互动不一定代表网页生命周期内的所有互动。此外,FID 仅测量首次互动的“输入延迟”部分,即浏览器在开始处理互动之前必须等待的时间(由于主线程繁忙)。
Interaction to Next Paint (INP) 用于通过观察用户在访问网页期间发生的所有符合条件的互动的延迟时间,评估网页对用户互动的总体响应情况。
INP 不仅会衡量首次互动,还会考虑所有互动,并报告网页整个生命周期内最慢的互动。此外,INP 不仅会测量延迟部分,还会测量从互动开始,一直到事件处理脚本,再到浏览器能够绘制下一帧的完整时长。因此是 Interaction to Next Paint。这些实现细节使得 INP 能够比 FID 更全面地衡量用户感知的响应能力。
从 2024 年 3 月开始,INP 将替代 FID 加入 Largest Contentful Paint (LCP) 和 Cumulative Layout Shift (CLS),作为三项稳定的核心网页指标。
INP 的计算方法是观察用户与网页进行的所有互动,而互动是指在同一逻辑用户手势触发的一组事件处理脚本。例如,触摸屏设备上的“点按”互动包括多个事件,如pointerup
、pointerdown
和click
。互动可由 JavaScript、CSS、内置浏览器控件(例如表单元素)或由以上各项驱动。
我们同样可以使用 Event Timing API 在 JavaScript 中衡量 FID:
1 | new PerformanceObserver((entryList) => { |
关于 INP 的优化,可以参考 Optimize Interaction to Next Paint。
web-vitals JavaScript 库 使用PerformanceObserver
,用于测量真实用户的所有 Web Vitals 指标,其方式准确匹配 Chrome 的测量方式,提供了上述提到的各种指标数据:CLS、FID、LCP、INP、FCP、TTFB。
我们可以使用 web-vitals 库来收集到所需的数据。
PSI 根据网页指标计划设置了阈值,将用户体验质量分为三类:良好、需要改进或较差,具体可参考 PageSpeed Insights 简介。
值得注意的是,PSI 报告所有指标的第 75 百分位。
为便于开发者了解其网站上最令人沮丧的用户体验,选择第 75 百分位。通过应用上述相同阈值,这些字段指标值被归类为良好/需要改进/欠佳。
这与我们常见的前端性能指标监控不大一样,因为一般来说大家会取平均值来评估指标。而取 75 百分位这种方式,值得我们去好好思考哪种计算方式更能真实反应用户的体验。
当然,上述 PSI 的性能指标体系,也未必完全适合我们网页使用,我们还可以针对网页的实际情况做出调整。举个例子,网页的 FCP/LCP 虽然十分影响用户的留存,但如果是对于专注服务于老用户、操作频繁、使用时长长的应用来说,网页运行过程中的流畅性更值得关注。
性能优化的事项很多,事情也往往很杂。当我们去针对我们网页进行性能优化事项的时候,如何评估我们的成果也是一个永恒不变的话题。
建立起有效的性能指标体系,就能更直观地展示出网页存在的性能问题,以及优化后的效果。
但需要注意的是,一味地追求指标数据并不都是一件好事情,因为为了指标好看往往我们会牺牲掉一些其他的体验。最终在平衡取舍下,呈现给用户最合适的体验才是开发的责任所在。
]]>PerformanceObserver
这玩意,不看不知道,越看越有意思。其实这个 API 出了挺久了,机缘巧合下一直没有接触到,直到最近开始深入研究前端性能情况。
其实单看PerformanceObserver
的官方描述,好像没什么特别的:
PerformanceObserver()
构造函数使用给定的观察者callback
生成一个新的PerformanceObserver
对象。当通过observe()
方法注册的条目类型的性能条目事件被记录下来时,调用该观察者回调。
乍一看,好像跟我们网页开发和性能数据没什么太大关系。
在很早的时候,前端开发的性能数据很多都是从Performance
里获取:
Performance
接口可以获取到当前页面中与性能相关的信息。它是 High Resolution Time API 的一部分,同时也融合了 Performance Timeline API、Navigation Timing API、User Timing API 和 Resource Timing API。
提到页面加载耗时,还是得祭出这张熟悉的图(来自PerformanceNavigationTiming API):
上述图中的数据都可以从window.performance
中获取到。
一般来说,我们可以在页面加载的某个结点(比如onload
)的时候获取,并进行上报。
但这仅包含页面打开过程的性能数据,而近年来除了网页打开,网页使用过程中的用户体验也逐渐开始被重视了起来。
2024 年 3 月起,INP (Interaction to Next Paint) 将替代 First Input Delay (FID) 加入 Largest Contentful Paint (LCP) 和 Cumulative Layout Shift (CLS),作为三项稳定的核心网页指标。尽管第一印象很重要,但首次互动(FID)不一定代表网页生命周期内的所有互动(INP)。
这意味着我们还需要关注整个网页生命周期内的用户体验,PerformanceObserver
的设计正是为了提供用户体验相关性能数据,它鼓励开发人员尽可能使用。
[PerformanceObserver
]{https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver} 对象为性能监测对象,用于监测性能度量事件,在浏览器的性能时间轴记录新的 performance entry 的时候将会被通知。
研究过前端性能的人,或许还有些对PerformanceObserver
不大熟悉(比如我),但是所有大概都知道 Chrome 浏览器的 Performance 性能时间轴:
作为 Performance 面板的老用户,我们常常会从时间轴上捞取出存在性能问题的操作,然后细细分析和研究对应的代码执行情况。而这个时间轴上记录下 performance entry 时,我们可以当通过observe()
方法获取到对应的内容和数据。
前面提到,如果我们需要关注网页在整个生命周期中的性能情况,意味着需要定期轮询、埋点等方式做上报。通过使用PerformanceObserver
接口,我们可以:
之前给大家讲过前端性能数据指标体系,我们能看到核心网页指标包括 FID、LCP 和 CLS,他们都可以从使用PerformanceObserver
直接拿到:
1 | // FID |
此外,web-vitals JavaScript 库可用来测量真实用户的所有 Web Vitals 指标,其方式准确匹配 Chrome 的测量方式。他提供了 PSI 中的各种指标数据:CLS、FID、LCP、INP、FCP、TTFB,如果你仔细研究它的实现,便是使用PerformanceObserver
的能力。
比如,INP 需要监控整个网页生命周期中的交互体验,我们可以看到其实现基于PerformanceEventTiming
的监测实现:
1 | new PerformanceObserver((list) => { |
而Event Timing API
中包括的用户交互事件几乎是很全的,但该方式可用于检测用户交互的流畅性,并不能作为出现卡顿时的定位方案。具体卡顿的定位,可参考《前端性能卡顿的监控和定位方案》一文。
在《前端性能卡顿的监控和定位方案》这篇文章中,我们还发现一个有意思的使用方式:
1 | new PerformanceObserver((resource) => { |
除了使用performanceObserver
监测resource
资源获取性能数据,我们还可以在回调触发时开始计数,以此计算该 JavaScript 资源加载耗时,从而考虑是否需要对资源进行更合理的分包。
配合PerformanceObserver
,我们还可以使用User Timing API
进行自定义打点:
1 | // Record the time immediately before running a task. |
然后使用PerformanceObserver
获取相关指标数据:
1 | // 有兼容性,需要处理异常 |
更多的使用方式,可以参考自定义指标一文。
由于PerformanceObserver
对象与浏览器的性能时间轴紧紧相关,基于此我们可以做很多性能监测的事情。
如果想偷懒,使用 web-vitals JavaScript 库并对 PSI 定义的核心指标进行上报,我们就能大概掌握了网页的核心性能指标数据,并以此进行分析和优化。
前端性能在前端领域中,也算是个亘古不变的难题,每次研究总能学到新的知识,这也是挺有趣的一件事呢。
]]>但是这活落到了咱们头上,老板说啥就得做啥。能本地复现的我们还能打开控制台,打个断点或者录制 Performance 来看看到底哪些地方占用较大的耗时。如果没法本地复现呢?
首先,我们来看看可以怎么主动检测卡顿的出现。
卡顿,顾名思义则是代码执行产生长耗时,导致浏览器无法及时响应用户的操作。那么,我们可以基于不同的方案,来监测当前页面响应的延迟。
对应浏览器来说,由于 JavaScript 是单线程的设计,当卡顿发生的时候,往往是由于 JavaScript 在执行过长的逻辑,常见于大量数据的遍历操作,甚至是进入死循环。
利用这个特效,我们可以在页面打开的时候,就启动一个 Worker 线程,使用心跳的方式与主线程进行同步。假设我们希望能监测 1s 以上的卡顿,我们可以设置主线程每间隔 1s 向 Worker 发送心跳消息。(当然,线程通讯本身需要一些耗时,且 JavaScript 的计时器也未必是准时的,因此心跳需要给予一定的冗余范围)
由于页面发生卡顿的时候,主线程往往是忙碌状态,我们可以通过 Worker 里丢失心跳的时候进行上报,就能及时发现卡顿的产生。
但是其实 Worker 更多时候用于检测网页崩溃,用来检测卡顿的效果其实还不如使用window.requestAnimationFrame
,因为线程通信的耗时和延迟导致该方案不大准确。
前面前端性能优化–卡顿篇有简单提到一些卡顿的检测方案,市面上大多数的方案也是基于window.requestAnimationFrame
方法来检测是否有卡顿出现。
window.requestAnimationFrame()
会在浏览器下次重绘之前调用,常常用来更新动画。这是因为setTimeout
/setInterval
计时器只能保证将回调添加至浏览器的回调队列(宏任务)的时间,不能保证回调队列的运行时间,因此使用window.requestAnimationFrame
会更合适。
通常来说,大多数电脑显示器的刷新频率是 60Hz,也就是说每秒钟window.requestAnimationFrame
会被执行 60 次。因此可以使用window.requestAnimationFrame
来监控卡顿,具体的方案会依赖于我们项目的要求。
比如,有些人会认为连续出现 3 个低于 20 的 FPS 即可认为网页存在卡顿,这种情况下我们则针对这个数值进行上报。
除此之外,假设我们认为页面中存在超过特定时间(比如 1s)的长耗时任务即存在明显卡顿,则我们可以判断两次window.requestAnimationFrame
执行间超过一定时间,则发生了卡顿。
使用window.requestAnimationFrame
监测卡顿需要注意的是,他是一个被十分频繁执行的代码,不应该处理过多的逻辑。
熟悉前端性能优化的开发都知道,阻塞主线程达 50 毫秒或以上的任务会导致以下问题:
因此,W3C 推出 Long Tasks API。长任务(Long task)定义了任何连续不间断的且主 UI 线程繁忙 50 毫秒及以上的时间区间。比如以下常规场景:
我们可以使用PerformanceObserver
这样简单地获取到长任务:
1 | var observer = new PerformanceObserver(function (list) { |
相比requestAnimationFrame
,使用 Long Tasks API 可避免调用过于频繁的问题,并且performance timeline
的任务优先级较低,会尽可能在空闲时进行,可避免影响页面其他任务的执行。但需要注意的是,该 API 还处于实验性阶段,兼容性还有待完善,而我们卡顿常常发生在版本较落后、性能较差的机器上,因此兜底方案也是十分需要的。
前面也提到,卡顿产生于用户操作后网页无法及时响应。根据这个原理,我们可以使用PerformanceObserver
监听用户操作,检测是否产生卡顿:
1 | new PerformanceObserver((list) => { |
这种方式的好处是避免频繁在requestAnimationFrame
中执行任务,这也是官方鼓励开发者使用的方式,它避免了轮询,且被设计为低优先级任务,甚至可以从缓存中取出过往数据。
但该方式仅能发现卡顿,至于具体的定位还是得配合埋点和心跳进行会更有效。
不管是哪种卡顿监控方式,我们使用检测卡顿的方案发现了卡顿之后,需要将卡顿进行上报才能及时发现问题。但如果我们仅仅上报了卡顿的发生,是不足以定位和解决问题的。
那么,我们可以通过打点的方式来大概获取卡顿发生的位置。
举个例子,假设我们一个网页中,关键的点和容易产生长耗时的操作包括:
那么,我们可以在这些操作的地方进行打点。假设我们卡顿工具的能力主要有两个:
1 | interface IJank { |
那么,当我们在页面加载的时候分别进行打点,我们的堆栈可能是这样的:
1 | _jankLogs = [ |
当卡顿心跳发现卡顿产生时,我们可以拿到堆栈的数据,比如当用户在批量操作之后发生卡顿,假设此时我们拿到堆栈:
1 | _jankLogs = [ |
这意味着卡顿发生时,最后一次操作是数据层--批量操作计算
,则我们可以认为是该操作产生了卡顿。
我们可以将module
/action
以及具体的卡顿耗时一起上报,这样就方便我们监控用户的大盘卡顿数据了,也较容易地定位到具体卡顿产生的位置。
当然,上述方案如果能达到最优效果,则我们需要在代码中关键的位置进行打点,常见的比如数据加载、计算、事件触发、JavaScript 加载等。
我们可以将打点方法做成装饰器,自动给class
中的方法进行打点。如果埋点数据过少,可能会产生误报,那么我们可以增加心跳的打点:
1 | IJank._heartbeat = () => { |
当我们心跳产生的时候,会更新堆栈数据。假设发生卡顿的时候,我们拿到这样的堆栈信息:
1 | _jankLogs = [ |
显然,卡顿发生时最后一次打点为Jank--heartbeat
,这意味着卡顿并不是产生于数据层---批量操作计算
,而是产生于该逻辑后的一个不知名逻辑。在这种情况下,我们可能还需要再在可疑的地方增加打点,再继续观察。
有一个用于监控一些懒加载的 JavaScript 代码的小技巧,我们可以使用PerformanceObserver
获取到 JavaScript 代码资源拉取回来后的时机,然后进行打点:
1 | performanceObserver = new PerformanceObserver((resource) => { |
当卡顿产生时,堆栈的最后一个日志如果为compileScript--bundle_xxxx
之类的,则可以认为该 JavaScript 资源在加载的时候耗时较久,导致卡顿的产生。
通过这样的方式,我们可以有效监控用户卡顿的发生,以及卡顿产生较多的逻辑,然后进行相应的问题定位和优化。
对于计算逻辑较多、页面逻辑复杂的项目来说,卡顿常常是一个较大痛点。
关于日常性能的数据监控和优化方案之前也有介绍不少,相比一般的性能优化,卡顿往往产生于不合理的逻辑中,比如死循环、过大数据的反复遍历等等,其监控和定位方式也与普通的性能优化不大一致。
]]>前端开发中相对基础的一些内容,主要围绕着 HTML/CSS/Javascript 和浏览器等相关。这些基础知识的掌握是必须的,但有些时候在工作中未必会用到。例如有些项目前后端部署在一起,并不会存在跨域一说,那么可能在开发过程中不会遇到浏览器请求跨域和解决方案相关问题。除了通过不断地学习和在项目中练习,或许从面试的角度来补齐相应的专业知识,可以给我们带来更大的动力。
本文的内容包括:
首先我们会针对前端开发相关来介绍需要掌握的一些知识,内容会包括 Javascript、HTML 与 CSS、网络相关、浏览器相关、安全相关、算法和计算机通用知识。
由于篇幅关系,下面会以关键知识点和问题的方式进行描述,并不会提供具体的答案和详细的内容描述。因此,大家可以针对提到的知识点和问题去进行深入学习和发散,也可以去网上搜一些相关的题目,结合大家的答案去尝试进行理解和解答。
关于 HTML 的内容会较少单独地问,更多是结合浏览器机制等一起考察:
关于 CSS,也有以下的一些考察点:
display
static
/relative
/absolute
/fixed
等z-index
与position
的作用关系animate
、transition
等很多时候,面试官也会通过让候选人编码实现某些样式/元素的方式,来考察候选人对 CSS 的掌握程度,其中布局(居中、对齐等)会比较容易考察到。
前端最基础的技能包括 Javascript、CSS 和 HTML,尤其是新人比较容易遇到这方面的考察。对于 Javascript 会问到多一些,通常包括:
考察范围 | 具体问题 |
---|---|
对单线程 Javascript 的理解 | 单线程来源 Web Workers 和 Service Workers 的理解 |
异步事件机制 | 为什么使用异步事件机制 在实际使用中异步事件可能会导致什么问题 关于 setTimeout 、setInterval 的时间精确性 |
对 EventLoop 的理解 | 介绍浏览器的 EventLoop 宏任务(MacroTask)和微任务(MicroTask)的区别 setTimeout 、Promise 、async /await 在不同浏览器的执行顺序 |
Javascript 的原型和继承 | 如何理解 Javascript 中的“一切皆对象” 如何创建一个对象 proto 与prototype 的区别 |
作用域与闭包 | 请描述以下代码的执行输出内容(考察作用域) 什么场景需要使用闭包 闭包的缺陷 |
this 与执行上下文 | 简单描述this 在不同场景下的指向apply /call /bind 的使用箭头函数与普通函数的区别 |
ES6+ | 对Promise 的理解使用 async 、await 的好处浏览器兼容性与 Babel Set 和Map 数据结构 |
对 Javascript 的考察,也可以通过写代码的方式来进行,例如:
call
/apply
/bind
Promise
、async
/await
网络相关的知识在日常开发中也是挺常用的,相关的问题可以从“一个完整的 HTTP 请求过程”来讲述,包括:
以上的内容都需要尽数掌握,除此以外,关于 HTTP 的还有以下常见内容:
关于浏览器,有很多的机制需要掌握。通常来说,面试官会从一个叫“在浏览器里面输入 URL,按下回车键,会发生什么?”中进行考察,首先会经过上面提到的 HTTP 请求过程,然后还会涉及以下内容:
考察内容 | 相关问题 |
---|---|
浏览器的同源策略 | “同源”指什么 那些行为受到同源策略的限制 常见的跨域方案有哪些 |
浏览器的缓存相关 | Web 缓存通常包括哪些 浏览器什么情况下会使用本地缓存 强缓存和协商缓存的区别 强制 ctrl +F5 刷新会发生什么session、cookie 以及 storage 的区别 |
浏览器加载顺序 | 为什么我们通常将 Javascript 放在<body> 的最后面为什么我们将 CSS 放在 <head> 里 |
浏览器的渲染原理 | HTML/CSS/JS 的解析过程 渲染树是怎样生成的 重排和重绘是怎样的过程 日常开发中要注意哪些渲染性能问题 |
虚拟 DOM 机制 | 为什么要使用虚拟 DOM 为什么要使用 Javascript 对象来描述 DOM 结构 简单描述下虚拟 DOM 的实现原理 |
浏览器事件 | DOM 事件流包括几个阶段(点击后会发生什么) 事件委托是什么 |
安全在实际开发中是最重要的,作为前端开发,同样需要掌握:
很多大公司会考察算法基础,大家其实也可以多上 leetcode 去刷题,这些题目刷多了就有感觉了。前端比较爱考的包括:
除此之外,常见的数据结构也需要掌握:
很多人会觉得,对前端开发来说算法好像并不那么重要,的确日常开发中也几乎用不到。但不管是前端开发也好,还是后台开发、大数据开发等,软件设计很多都是相通的。一些比较著名的前端项目中,也的确会用到一些算法,同样树状数据结构其实也在前端中比较常见。
同样的,虽然在日常工作中我们接触到的内容比较局限于前端开发,但以下内容作为开发必备基础,也是需要掌握的:
基础知识相关的内容真的不少,但是这块其实只要准备足够充分就可以掌握。参加过高考的我们,理解和记忆这么些内容,其实没有想象中那么难的。
项目经验通常和个人经历关系比较大,前端业务相关的的一些项目经验通常包括管理端、H5 直出、Node.js、可视化,另外还包括参与工具开发的经验,方案选型、架构设计等。
项目相关的内容,比如性能优化、前端框架之类的,之前我也整理过不少的文章,欢迎大家自己翻阅哦~
首先我们来看看前端框架,不管你开发管理端、PC Web、H5,还是现在比较流行的小程序,总会面临要使用某一个框架来开发。因此,以下的问题可能与你有关:
如果你使用到了小程序,还可能会问到:
而工具库相关的就太多了,一般会这么问:
项目相关的许多问题,其实是我们工作中经常会遇到并需要进行思考的问题。如果平时有养成思考和总结的习惯,那么这些问题很容易就能回答出来。如果平时工作中比较少进行这样的思考,也可以在面试准备的时候多关注下。
Node.js 相关的可能包括:
Process.nextTick
和setImmediate
的区别以上这些都属于很基础的问题。很多时候,我们会使用 Node.js 去做一些脚本工程或是服务端接入层等工作。如果项目中有使用 Node.js,面试官更多会结合项目相关的进行提问。
性能优化的其实跟项目比较相关,常见的包括:
很多时候,性能优化也是与项目本身紧紧相关,一般来说会包括首屏耗时优化、页面内容渲染耗时优化、内存优化等,可能涉及代码包大小、下载耗时、首屏直出、存储资源(内存/indexDB)等内容。
如今前端工程化的趋势越来越重,通常从脚手架开始:
除了脚手架相关,如今自动化、流程化的使用也越来越多了:
工程化和自动化是如今前端的一个趋势,由于团队协作越来越多,如何提升团队协作的效率也是一个可具备的技能。
效能提升的意识在工作中很重要,大家都不喜欢低效的加班。通常可能问到的问题包括:
发布和监控这部分,可能较大的业务才会有,涉及的问题可以有:
对于大型项目来说,灰度发布几乎是开发必备,而监控和问题定位也需要各式各样的工具来辅助优化。
一些较大的项目,通常由多个开发合作完成。而多人协作的经验也很有帮助:
看到这么多内容不要慌,一般来说面试官只会根据你的工作经历来询问对应的问题,所以如果你并没有完全掌握某一块的内容,请不要写在简历上,你永远也不知道面试官会延伸到哪。
专业知识也好,项目经验也好,充分的准备可以留给面试官不错的印象。但这些都未必能完全体现日常工作和思考的一些能力,面试官通常会通过编程题、逻辑思维开放题等其他角度来。
同时,对于程序员来说,自学是很关键的一个能力,面试官也可能会通过职业规划、学习习惯等角度,了解候选人对技术的热情、是否好学、抗压能力、解决问题能力等,来判断候选人是否符合团队要求、是否适合团队氛围。
而从面试的角度来介绍这些内容,除了可以有方向地进行知识储备,更多的是希望大家能结合自身的实际情况反思自己是否还有可以改善的地方,因为面试过程中考察的点通常便是实际工作中会遇到的问题。
最后,圣诞夜,祝各位顺颂冬安~
]]>在《复杂渲染引擎架构与设计–3.底层渲染适配》一文中,我们介绍了不同的渲染方式,包括 Canvas 渲染、DOM 渲染、SVG 渲染甚至是 WebGL 渲染等。对于不同的渲染方式,要实现元素选取的代价十分不一样。
对于 DOM/SVG 渲染,我们可以直接使用浏览器提供的元素选择能力。在这样的场景下,不管是父子元素的管理、事件冒泡和捕获等都比较容易实现。因此,我们今天主要讨论 Canvas 渲染要如何实现元素选择。
对于 Canvas 渲染里进行元素选取,我们常见有几种方式:
几何检测法是许多游戏引擎或者说物理引擎的解决方案,我们常常又称为碰撞检测法。
在元素选取的场景下,我们只需要判断用户触发事件的位置,是否落在某个元素几何里。因此,我们面临的问题是:确定某个点是否位于给定的多边形内。
一般来说,某个点是否在某个多边形内,常见的便是交叉数法(也称射线判断法):从所讨论的点 P 向任何方向射出一条射线(半线),判断该射线与元素几何相交的线段数奇偶情况。在该方法中,我们需要确保射线不会直接射到多边形的任何顶点,这个是比较难做到的。因此,也有不少改良的方案,具体可以参考《When is a Point Inside a Polygon?》。
除了交叉数法,还有环绕数法,这里不进行详细解释了,具体可以参考《Canvas 中判断点是否在图形上》。
几何检测法在渲染引擎中使用,优势在于内存消耗小。但它也存在一些问题:
除此之外,如果元素存在堆叠的情况,则可能需要遍历地进行检测判断;如果存在的元素数量特别庞大,则意味着这样的遍历性能可能会受到影响。
像素检测法又称色值法,简单来说就是给每个绘制的图案按照堆叠顺序绘制一个带随机背景色的一样的图案,当某个点落在 Canvas 上时,则可以通过所在的像素颜色,找到对应的几何元素。
这个方法看似简单,实际上我们需要使用两个 Canvas 来实现:
当用户进行交互时,通过 Canvas 位置找出第二个 Canvas 的颜色,然后根据色值去获取到对照的图形。这便要求我们每个图形在绘制前,都需要生成一个元素与随机色值的映射表,通过该表才能获取到最终的元素。
像素检测法的实现很简单,这也是它的优势,但是同样会存在一些问题:
如果考虑到像在线表格这样的产品,由于还需要滚动和重绘,像素检测法的性能或许不会很好。而表格本身就是天然四方形的布局,因此更适合使用几何检测法,而在使用几何检测法的时候,我们甚至只需要判断某个点是否落在某个矩形内,几乎涉及不到较复杂的算法。
对于一些复杂的交互场景,我们可以适当地添加 DOM 元素来降低维护成本。
比如,在线表格的交互中,很多时候我们都需要先选中一些格子,然后再进行操作。那么,我们可以先使用简单的几何检测法来获取到对应单元格位置,然后生成一个对应的 DOM 元素覆盖在对应的 Canvas 上,之后所有的交互都由这个 DOM 元素来完成。
显然,像输入编辑这种功能,是无法完全使用 Canvas 实现的,或者说是成本巨大,因此我们可以直接使用一个 DOM 的编辑框放在 Canvas 上面,等用户完成编辑操作,再把内容同步到 Canvas 上即可。
这是一种比较简单又取巧的解决方案,但同样需要考虑一些问题:
本文主要介绍了 Canvas 里实现元素获取和事件处理的几种解决方案。
其实我们并不是所有时候都需要硬啃复杂的算法或是解决方案,换一下思路,其实你会发现有无数的方向可以尝试。虽然很多时候,我们常说要参考业界常见成熟的方案,但这并不意味着我们就一定要照抄。
适合自己的才是最好的,不管它是大众的方案,还是某种特殊场景下的解决方案。
]]>本文我们将结合 Canvas 的能力提出进一步的优化方案:离屏渲染。
上一篇《6.增量渲染》提到页面滚动时 Canvas 复用的场景,这种场景下我们还可以考虑两种方式:
第二种方式中,当前渲染的 Canvas 与隐藏的缓存 Canvas 交替渲染,由于会使用一个屏幕外(非可视)的 Canvas 进行提前绘制,我们也可以称之为离屏渲染。
离屏渲染可以提前将更大范围的内容绘制好,在滚动时可直接取对应的区域进行截取和绘制。
当然,两个 Canvas 的维护和绘制成本会比一个 Canvas 要更高,同时如果需要提前绘制更大区域的单元格范围,那么必然会面临一个问题:需要更多的计算和渲染消耗。
我们可以考虑另外一个优化方案:使用 OffscreenCanvas 实现真正的离屏。
OffscreenCanvas 是一个实验中的新特性,主要用于提升 Canvas 2D/3D 绘图应用和 H5 游戏的渲染性能和使用体验。OffscreenCanvas 目前主要用于两种不同的使用场景:
整体的离屏方案依赖 OffscreenCanvas 提供的能力,关于此能力现有的技术方案和文档较少,可参考:
在我们的架构设计下,更适合使用第一种方案,即同步显示 OffscreenCanvas 中的帧。这样设计的优势在于:当主线程繁忙时,依然可以通过 OffscreenCanvas 在 worker 中更新画布内容,避免给用户造成页面卡顿的体验。
除此之外,还可以进一步考虑在兼容性支持的情况下,通过将局部计算运行在 worker 中,减少渲染引擎的计算耗时,提升渲染引擎的渲染性能。
当然,如果要实现在 Worker 中进行提前渲染,则需要考虑如何将渲染引擎提供给 Worker,以及将数据及时同步到 Worker 的问题。
如果想完全发挥到 OffscreenCanvas 的作用,要支持真正意义上的离屏渲染,而不是在主线程使用一个隐藏的 Canvas 交替绘制,需要考虑:
由于渲染引擎本身是需要实时响应用户的操作的,因此大部分的内容更新是需要同步计算、并更新到 Canvas 中的。如果提取到 worker 中进行,需要考虑是否由于线程通信的原因导致响应速度的降低,反而影响用户体验。
方向一:每次有数据更新,渲染引擎都会全量更新和计算,可以考虑将非可视区域范围的部分(即可视范围往后的部分)放置到 worker 和离屏 Canvas 中进行计算
方向二:前面提到,渲染引擎的渲染分为两部分:
对于插件部分内容,可以考虑将其放到 worker 中计算并更新。但局部内容异步渲染,可能需要考虑对当前 Canvas 进行改造,进行分层渲染,即可按照堆叠顺序进行 Canvas 拆分,结合每块内容的更新频率,仅更新某种类型的绘制内容。
对于项目中是否适合使用该离屏方案,需要结合项目自身的架构设计、改造成本和兼容性问题等情况,考虑好上述问题,才能决定。即使是在 Worker 中不阻塞主线程,依然需要考虑计算量过大可能会导致渲染延迟等问题。
它会带来不小的改造成本,但收益是否可观还需要观察,你也可以先编写一个 demo 来确认效果,再尝试在项目中接入使用。
]]>因此,我们可以考虑设计一套增量渲染的能力,来实现改多少、重绘多少,减少每次渲染的耗时,提升用户的体验。
所谓增量渲染,或许你已经从 React/Vue 等框架中有所耳闻,即更新仅需要更新的部分内容,而不是每次都重新计算和渲染。
React 里结合了虚拟 DOM 以及 Fiber 引擎来实现完整的 Diff 计算和渲染调度,这些我之前在其他文章也有说过。在 React 里,状态的更新机制主要由两个步骤组成:
我们的渲染引擎道理也是十分相似的,即找出最小变化范围进行计算和更新。同样的,我们还是继续以在线表格为例子,基于我们现有的引擎设计上实现增量渲染。
关于渲染引擎的收集和渲染过程,已经在前面《1.收集与渲染》文章中介绍过。
基于该架构设计,我们知道一次渲染分成两个过程:
而前面在《2.插件的实现》中也提到,渲染引擎整体的架构如图:
在该架构图中,渲染引擎支持提供绘制特定范围的能力。而要实现这样的能力,我们需要做到:
由于在线表格这样的产品都是以单元格为基础,因此我们的收集器和渲染器都同样可以以单元格为最小单位,提供以下的能力:
实际上,一次渲染的耗时大头更多会出现在收集过程,因为收集过程中常常会进行较复杂的计算,亦或是针对一个个格子的数据收集会导致不停地遍历各个格子范围和访问特定对象获取数据。
所以更重要的增量能力在于收集过程的增量。
对于在线表格的场景,我们可以考虑两种增量渲染的情况:
我们分别来看看。
局部修改比较简单,前面我们已经提到说收集过程和渲染过程都支持按指定范围进行增量渲染,因此局部修改的时候直接走特定范围的绘制即可。比如用户修改了 A1 这个格子:
页面滚动与纯某个特定范围的修改不大一样,因为页面滚动过程中,所有单元格的位置都会发生改变。
一般来说,在滚动过程我们会产生局部可复用的单元格绘制结果,如图:
对于这样的情况,我们可以有两种解决方案:
方案 1 直接复用局部 Canvas 的方案比较简单,不少在线表格像谷歌表格、飞书文档等都是用的该方案,该方案同样存在一些问题:
方案 2 复用局部收集的渲染数据结构,可以优化上述问题,但整体的性能会比复用 Canvas 稍微差一些,毕竟复用 Canvas 直接节省了复用范围的收集和渲染耗时,而复用收集结果则仅节省了复用范围的收集,绘制过程还是会全量绘制。
对于复用收集结果的方案,还需要考虑页面出现滚动导致的绘制位置差异,即使是同一个单元格,其在画布上的位置也发生了改变,这样的变化需要考虑进去。
因此,收集器的数据结构需要和单元格紧密相关,而不是基于 Canvas 的整体偏移。如何设计出性能较好又易于理解的数据结构,这也是一项不小的挑战,决定了我们增量渲染的优化效果能到哪里。
由于渲染引擎和用户视觉、交互紧密相关,因此常常是性能优化的大头。结合产品特点和架构设计做具体的分析和优化,这才是我们在实际工作中常常面临的挑战。
前面介绍过的分片优化也好,这里的增量渲染也好,其实大多数都能在业界找到类似的思路来做参考。不要把思路局限在相同产品、相同场景下的解决方案,即使是看似毫不相干的优化场景,你也能拓展思维看看对自己遇到的难题是否能有所启发。
]]>本文我们以在线表格为例子,详细介绍下如何对长耗时的计算进行任务拆解。
在表格中,当数据发生更新变化时(可能是用户本身的操作,也可能是协作者),渲染引擎接收到数据变更,然后进行计算和更新渲染。流程如下:
上述的步骤 2 中,渲染引擎计算均为同步计算,因此随着计算范围的增加,所需耗时会随之增长。
在这样的基础上,我们提出了将渲染引擎计算任务进行分片的方案。该方案主要优化的位置位于渲染引擎的计算过程,可减少在大范围、大表下的操作(如列宽调整、大范围选区的样式设置等)卡顿。
本次渲染引擎计算任务分片的方案核心点在于:只进行可视区域的渲染计算,非可视区域的部分做异步计算。
如图,当一次数据变更发生时,渲染引擎会根据变更范围,将计算任务拆成两部分:可视区域和非可视区域的计算任务。
整个计算异步分片方案中,有以下几个核心设计点:
我们来看一下其中的待计算区域管理和异步任务管理的部分设计。
首先,我们提供了一个区域管理的能力,里面存储了未计算完成的区域。区域管理的能力需要满足:
由于渲染引擎计算的特殊性(大多数计算为按行计算),区域考虑以行为首要维度、列为次要维度的方式来管理,因此区域的设计大概为:
1 | export type IAreaRange = { |
对于两个区域的合并,需要考虑相交和不相交的情况。不相交时不需要做合并,而对于相交的情况,还需要考虑合并的方式,主要考虑单边相交和包含关系的合并:
根据计算类型和列范围,且考虑边界场景下,两个区域合并后可能会转换为 1/2/3 个区域。
由于区域本身依赖了行列位置,因此当行列发生改变时,比如插入/删除/隐藏/移动(即插入+删除)等场景,我们需要及时更新区域。以行变化为例:
同样需要考虑边界场景,比如删除区域覆盖了整个(或局部)区域等。
异步任务管理的设计采用了十分简洁的方式(一个setTimeout
任务)来实现:
1 | class AsyncCalculateManager { |
上述代码可以看到,每个任务执行耗时满 50ms 后,会结束当前任务,并设置下一个异步任务。通过这样的方式,我们将每次计算任务控制在 50ms 左右,避免计算过久而导致的卡顿问题。
对于异步任务,每次执行的时候,都需要:
对于 1,可视区域内如果存在未计算的任务,会以符合阅读习惯的从上到下进行计算;如果可视区域内均已计算完毕,则会以可视区域为中心,向两边寻找未计算任务,并进行计算。如图:
异步任务计算时,还需要考虑计算的范围是否涉及可视区域,如果在可视区域内有计算任务,则需要进行渲染;如果计算任务处于非可视区域,则可以避免进行不必要的渲染。
将原本同步计算的任务拆成多个异步的计算任务,会面临一些问题包括:
解决方案大概是:确保每次计算后,行列宽高、可视区域、画布偏移等位置数据的一致性。要做到所有数据的一致性,需要对各个节点的流程做整体梳理,这里就不详细展开了。
本文以在线表格的分片计算为例,详细介绍了如何将大的计算任务拆分成小任务,减少了渲染等待的计算耗时。
我们常常会将产品和技术分离,认为技术需求占用了产品需求的人力,或是认为产品需求导致技术频繁变更。实际上,技术依附于产品而得以实现,产品亦是需要技术作为支撑。
每一个项目都需要不断地打磨,我们在产品快速向前迭代的同时,也需要实时关注项目本身的基础能力是否能满足产品未来的规划和方向。
]]>本文我们详细针对复杂计算的场景来考虑渲染引擎的优化。
对于需要进行较复杂计算的渲染场景,结合收集和渲染的架构设计,我们完整的渲染流程大概应该是这样的:
可见,完整的渲染流程里,计算的复杂程度会直接影响渲染是否及时,最终影响到用户的交互体验。在这里,我们还是以在线表格为例子,详细介绍下为什么有如此大的计算任务。
在表格中,画布绘制所需的数据,并不能完全从数据层中获取得到。对于以下一些情况,需要经过渲染引擎的计算处理才能正确绘制到画布上,包括:
如下图,当单元格设置了自动换行,当格子内容超过一行会被自动换到下一行。由于内容宽度的测量依赖浏览器环境,因此也是需要在渲染引擎进行计算的:
当某个行没有设置固定的行高时,该行内容的高度可能会存在被自动换行的单元格撑高的情况,因此真实渲染的行高也需要根据分行/换行结果进行计算。
如下图,在没有设置自动换行的情况下,当单元格内容超出当前格子,会根据对齐的方向、该方向上的格子是否有内容,向对应的方向拓展内容,呈现向左右两边覆盖的情况:
受覆盖格影响,覆盖格和隐藏格(即被覆盖的格子)间的边框线会被超出的内容遮挡,因此对应的边框线也会受影响。
以调整列宽为例子,该操作涉及的计算包括:
可见,除了分行计算只涉及该列格子,一次列宽操作几乎涉及全表内容的计算,在大表下可能会导致几秒的卡顿,在一些低性能的机器上甚至会达到十几秒。由于该过程为同步计算,网页会表现为无响应,甚至严重的情况下会弹窗提示。
之前我在《前端性能优化–卡顿篇》一文中,有详细介绍对于大任务计算的优化方向,包括:
对于较大型的前端应用,即使并非使用 Canvas 自行排版,依然可能会面临计算耗时过大的计算任务。当然,更合理的方式是将这些计算放在后台进行,直接将计算完的结果给到前端使用。
也有一些场景,尤其是前端与用户交互很重的情况下,比如游戏和重编辑的产品。这类产品无法将计算任务放置在后端,甚至无法将计算任务拆分到 Web Worker 进行计算,因为请求的等待耗时、Worker 的通信耗时都会影响用户的体验。
对该类产品,最简单又实用的方法便是:拆。
将计算任务做拆分,我们可以结合计算场景做分析,比如:
比如,React16 中新增了调度器(Scheduler),调度器能够把可中断的任务切片处理,能够调整优先级,重置并复用任务。
调度器会根据任务的优先级去分配各自的过期时间,在过期时间之前按照优先级执行任务,可以在不影响用户体验的情况下去进行计算和更新。
通过这样的方式,React 可在浏览器空闲的时候进行调度并执行任务。
还有一种同样常见的方式,便是将计算任务进行拆分后,通过预判用户行为,提前执行将用到的计算任务。
举个例子,当前屏幕内的数据都已计算和渲染完毕,页面加载处于空闲时,可以提前将下一屏幕的资源获取,并进行计算。
这种预计算和渲染的方式,有些场景下也会称之为离屏渲染。离屏渲染同样可以作用于 Canvas 绘制过程,比如使用两个 Canvas 进行交替绘制,或是使用 worker 以及浏览器提供的 OffscreenCanvas API,提前将要渲染的内容计算并渲染好,等用户进入下一屏的时候可以直接拿来使用。
如果是页面滚动的场景,还可以考虑复用滚动过程中重复的部分内容,来节省待计算和渲染的任务数量。
这些方案,我们后面都会详细进行一一讨论,本文就不过多描述了。
或许很多开发同学都会觉得,以前没有接触过大型的前端项目,或是重交互重计算的产品,如果遇到了自己不知道该怎么做优化。
实际上,大多数的优化思路都是相似的,但是我们需要尝试跨越模板,将其应用在不同的场景下,你就会发现能得到许多想象以外的优化效果。
纸上得来终觉浅,绝知此事要躬行。不要自己把自己局限住了哟~
]]>关于渲染引擎整体架构和插件架构的设计,已在《收集与渲染》、《插件的实现》两篇文章中介绍过,渲染引擎架构如图:
底层渲染引擎由收集器和渲染器组成,其中收集器收集需要渲染的渲染数据,渲染器则负责将收集到的数据进行直接渲染。
本文我们将会介绍渲染器的多种渲染方式的适配,其中常见的就包括:
对于多种渲染方式的适配,架构设计上还比较简单:
从收集器收集到的数据,通过适配的方式,转换成不同的绘制结果。举个例子,同样是一个单元格内容:
<div>
元素或是<table>/<tr>/<td>
等表格元素来绘制一般来说,我们如果使用多种渲染方式,还需要考虑渲染一致性。渲染一致性是指,使用 Canvas 绘制的结果,需要与 SVG、DOM 绘制渲染的结果保持一致,不能出现太大的跳动或是位置、样式不一致的结果。
因此,我们在进行渲染的时候,根据选择的渲染方式,还需要做不同的兼容适配。
以上几种渲染方式中,DOM 渲染会受浏览器自身的排版引擎影响,这种影响可能是正面的,也可能是负面的。比如,我们 Canvas 排版方式是尽量接近浏览器原生的方式,那么当我们适配 DOM 渲染的时候则比较省力气。但如果说像在线表格这种场景,使用 DOM 进行表格的排版,则可能会遇到比较多的问题。
举个例子,我们都知道 DOM 里的表格元素(<table>
/<tr>
/<td>
等)是最难驾驭的,因为浏览器对它们的处理总是在意料之外,宽高难以控制意味着我们将很难将其与 Canvas/SVG 的渲染效果对齐。因此,我们很可能需要在表格元素里嵌套绝对定位的<div>
元素,来使得表格最终渲染不会被轻易撑开导致偏差。
除此之外,我们还需要注意文字的排版、换行等情况在 Canvas/SVG 和 DOM 渲染中需要尽量保持一致。
每种渲染方式都有各自的优缺点。
在图表渲染引擎中,最常见的是 Canvas 渲染和 SVG 渲染,我们也可以从 ECharts 官网中找到两者的对比描述:
- 一般来说,Canvas 更适合绘制图形元素数量较多(这一般是由数据量大导致)的图表(如热力图、地理坐标系或平行坐标系上的大规模线图或散点图等),也利于实现某些视觉特效。
- 但在不少场景中,SVG 具有重要的优势:它的内存占用更低(这对移动端尤其重要)、并且用户使用浏览器内置的缩放功能时不会模糊。
选择哪种渲染器,可以根据软硬件环境、数据量、功能需求综合考虑:
- 在软硬件环境较好,数据量不大的场景下,两种渲染器都可以适用,并不需要太多纠结
- 在环境较差,出现性能问题需要优化的场景下,可以通过试验来确定使用哪种渲染器。比如:
- 在需要创建很多 ECharts 实例且浏览器易崩溃的情况下(可能是因为 Canvas 数量多导致内存占用超出手机承受能力),可以使用 SVG 渲染器来进行改善
- 如果图表运行在低端安卓机,或者我们在使用一些特定图表如水球图等,SVG 渲染器可能效果更好
- 数据量较大(经验判断 > 1k)、较多交互时,建议选择 Canvas 渲染器
而在在线表格的场景,我们会发现不同的团队会选择不同的渲染方式:
其实我们可以发现,这些团队很多在使用几种渲染方式,原因几乎都是因为使用了 Canvas 绘制作为主要渲染方式。但考虑到首屏渲染的情况,Canvas 则需要一系列的数据计算和渲染过程,不适合首屏直出的方式,因此会适配上 DOM 或者 SVG 进行首屏直出。
实际上,Canvas 渲染有一个比较致命的弱点:交互性很差。比如用户选择某个格子,进行拖拽、调整宽高、右键菜单等操作,在 Canvas 上是很难命中具体的元素的。因为 Canvas 绘制过程中并不像 DOM 和 SVG 一样有层次结构,最终的渲染结果也只是一个图像。因此,在线表格场景下大多数 Canvas 绘制都需要结合 DOM 引擎一起,获取到用户选择的元素、处理用户交互事件,然后进行二次计算和响应。
关于首屏直出,后面有空也可以简单唠唠。
本文介绍了渲染引擎架构中,使用多种渲染方式以及底层渲染器适配的设计。
我们常常说给项目选择最优的解决方案,实际上我们也会发现,正因为往往没有所谓最优解,这些产品才会针对不同的场景下提供了不同的解决办法。比如,考虑到性能问题 ECharts 提供了 Canvas/SVG 两种绘制方式;又比如考虑到首屏直出的效率,各个在线表格的团队分别适配了更合适的渲染方式。
]]>在前端业务领域中,除了大型开源项目(热门框架、VsCode、Atom 等)以外,协同编辑类应用(比如在线文档)、复杂交互类应用(比如大型游戏)等,都可以称得上是大型前端项目。对于这样的大型前端项目,我们在开发中常常遇到的问题包括:
其实大家也能看到,大型前端项目中主要的问题便是“管理混乱”。所以我个人觉得,对于代码管理得很混乱的项目,你也可以认为是“大型”前端项目(笑)。
对于代码量过大(比如高达 30W 行)的项目,如果不做任何优化直接全量跑在浏览器中,不管是加载耗时增加导致用户等待时间过久,还是内存占用过高导致用户交互卡顿,都会给用户带来不好的体验。
性能优化的解决方案在《前端性能优化–归纳篇》一文中也有介绍。其中,对于代码量、文件过多这样的性能优化,可以总结为两个字:
项目代码量过大不仅仅会影响用户体验,对于开发来说,代码开发过程中同样存在糟糕的体验:由于代码量过大,开发的本地构建、编译都变得很慢,甚至去打水 + 上厕所回来之后,代码还没编译完。
从维护角度来看,一个项目的代码量过大,对开发、编译、构建、部署、发布流程都会同样带来不少的压力。因此除了浏览器加载过程中的代码拆分,对项目代码也可以进行拆分,一般来说有两种方式:
1. multirepo,多仓库模块管理,通过工作流从各个仓库拉取代码并进行编译、打包。
npm link
有奇效);模块变动后,需要更新相关仓库的依赖配置(使用一致的版本控制和管理方式可减少这样的问题)2. monorepo,单仓库模块管理,可使用 lerna 进行包管理。
两种包管理模式各有优劣,一般来说一个项目只会采用其中一种,但也可以根据具体需要进行调整,比如统一的 UI 组件库进行分仓库管理、核心业务逻辑在主仓库内进行拆包管理。
题外话:很多人常常在争论到底是单仓好还是多仓好,个人认为只要能解决开发实际痛点的仓,都是好仓,有时候过多的理论也需要实践来验证。
不同的模块需要进行分工和配合,因此相互之间必然会产生耦合。在大型项目中,由于模块数量很多(很多时候也是因为代码量过多),常常会遇到模块耦合过于严重的问题:
对于模块耦合严重的模块,常见的解耦方案比如:
使用事件驱动的方式,可以快速又简单地实现模块间的解耦,但它常常又带来了更多的问题,比如:
我们还可以使用依赖倒置进行依赖解耦。依赖倒置原则有两个,包括:
使用以上方式进行设计的模块,不会依赖具体的模块和细节,只按照约定依赖抽象的接口。
如果项目中有完善的依赖注入框架,则可以使用项目中的依赖注入体系,像 Angular 框架便自带依赖注入体系。依赖注入在大型项目中比较常见,对于各个模块间的依赖关系管理很实用,比如 VsCode 中就有使用到依赖注入。
在 VsCode 中,我们也可以看到使用了依赖注入框架和标准化的Event/Emitter
事件监听的方式,来对各个模块进行解耦(可参考《VSCode 源码解读:事件系统设计》):
Disposable
,统一管理相关资源的注册和销毁this._register()
注册事件和订阅事件,将事件相关资源的处理统一挂载到dispose()
方法中使用依赖注入框架的好处在于,各个模块之间不会再有直接联系。模块以服务的方式进行注册,通过声明依赖的方式来获取需要使用的服务,框架会对模块间依赖关系进行分析,判断某个服务是否需要初始化和销毁,从而避免了不必要的服务被加载。
在对模块进行了解耦之后,每个模块都可以专注于自身的功能开发、技术优化,甚至可以在保持对外接口不变的情况下,进行模块重构。
实际上,在进行代码编程过程中,有许多设计模式和理念可以参考,其中有不少的内容对于解耦模块间的依赖很有帮助,比如接口隔离原则、最少的知识原则/迪米特原则等。
除了解决问题,还要思考如何避免问题的发生。对于模块耦合严重这个问题,要怎么避免出现这样的情况呢?其实很依赖项目管理的主动意识和规范落地,比如:
在对模块进行拆分和解耦、使用了模块负责人机制、进行包拆分管理之后,虽然开发同学可以更加专注于自身负责模块的开发和维护,但有些时候依然无法避免地要接触到其它模块。
对于这样大型的项目,维护过程(熟悉代码、定位问题、性能优化等)由于代码量太多、各个函数的调用链路太长,以及函数执行情况黑盒等问题,导致问题定位异常困难。要是遇到代码稍微复杂点,比如事件反复横跳的,即使使用断点调试也能看到眼花,蒸汽眼罩都得多买一些(真的贵啊)。
对于这些问题,其实可以有两个优化方式:
这个过程,其实是将模块负责人的知识通过工具的方式授予其他开发,大家可以快速找到某个模块经常出问题的地方、模块执行的关键点,根据建议和提示进行问题定位,可极大地提升问题定位的效率。
除了问题定位以外,各个模块和函数的调用关系、调用耗时也可以作为系统功能和性能是否有异常的参考。之前这块我也有简单研究过,可以参考《大型前端项目要怎么跟踪和分析函数调用链》。
因此,我们还可以通过将调用堆栈收集过程自动化、接入流水线,在每次发布前合入代码时执行相关的任务,对比以往的数据进行分析,生成系统性能和功能的风险报告,提前在发布前发现风险。
即使在项目代码量大、项目模块过多、耦合严重的情况下,项目还在不断地进行迭代和优化。遇到这样的项目,基本上没有一个人能熟悉所有模块的所有细节,这会带来一些问题:
导致这些问题的根本原因有两个:
对于这种情况,可以使用模块负责人的机制来对模块进行所有权分配,进行管理和维护:
通过模块负责人机制,每个模块都有了对应的开发进行维护和优化,开发也可以专注于自身的某些模块进行功能开发。在人员离职和工作内容交接的时候,也可以通过文档 + 负责人权限的方式进行模块交接。
大型项目的这些痛点,其实只是我们工作中痛点的缩影。技术上能解决的问题都是小事,管理和沟通上的事情才更让人头疼。
除此之外,在我们的日常工作中,通常也会局限于某块功能的实现和某个领域的开发。如果这些内容并没有足够的深度可以挖掘,对个人的成长发展也可能会有限制。在这种情况下,我们还可以主动去了解和学习其它领域的知识,也可以主动承担起更多的工作内容。
]]>本文将介绍渲染插件的设计,渲染插件可用于各种新特性的拓展绘制。
我们设计了渲染引擎,只能满足基础图形的收集和绘制,包括文本、线段、矩形、图像等。
而应用到具体的业务中,则是使用这些基础图形来绘制出业务相关的内容。因此,我们可以考虑将基础的能力进行封装,提供更便利的能力给到业务侧使用。
举个例子,依然是表格的场景,由于大多数内容都是以单元格为基本单位来进行绘制的,我们则可以封装出一个提供按单元格绘制的中间层能力。
而当业务侧进行编辑操作时,更新的范围除了单个格子,也会包括整行、整列、整表、所选区域等情况,因此我们可以封装给到业务侧这些能力。
到这里,渲染引擎架构如图:
这种分层能力不仅在渲染引擎中可以用到,即使在我们平时的页面开发中,也完全可以用到。最常见的包括将页面布局做分层拆分,然后进行渲染。
不过现在基本上渲染的流程和实现都交由前端框架来负责,而 DOM 的布局则都交给浏览器本身的排版引擎去处理,用 Canvas 来绘制布局的场景的确很少。
除了给上层业务提供封装好的能力,业务侧可以指定单元格范围进行重新渲染以外,还需要考虑另外一种的业务拓展场景:业务需要在单元格内绘制自己的内容,比如单元格背景高亮、一些特殊的图形属性单元格、图片绘制等等。
所以我们还需要给业务提供控制单元格绘制内容的能力。
前面一篇文章我们提到,每个单元格的绘制会有堆叠顺序,比如先绘制背景色,再绘制文字、边框线等等。那么,如果我们要给业务侧提供绘制的能力,他们同样需要可控制的堆叠顺序,和绘制内容的控制。
既然我们将渲染过程分成了收集和渲染两部分,渲染器的能力可以说是通用的能力,因为不管是单元格本身的绘制,还是业务侧新增的绘制内容,都离不开最基本的文字绘制、线段绘制、图形绘制、图像绘制等能力。
因此,我们可以考虑在收集过程中,通过提供插件的能力,让业务侧把想要绘制的内容收集起来。
插件提供的能力包括:
插件的收集流程大概如图:
简单来说,插件的实现可能是:
1 | // 该代码只是写了个思路,不作为最终的实现方式 |
通过这样的方式,业务可以自由地控制某些范围内单元格的绘制内容,且不需要侵入性地修改核心的绘制流程,拓展性得到了很好的提升。
现在越来越多的软件都支持通过插件的方式来拓展能力,也允许开发者一起来打造插件体系。对于插件的设计来说,独立性、安全性、拓展性都是比较重要的考虑方向。
本文结合收集和渲染的渲染架构,设计了一套方便业务拓展的底层能力,包括提供支持可选范围的重新渲染能力,以及控制单元格绘制内容的插件能力。
很多时候我们都关注核心的架构能力,而往往忽略了业务的快速发展和迭代。实际上,架构就是为了不断变化的业务服务的,因此架构设计的时候,保留符合业务发展需要的拓展能力也是十分必要的。
]]>而在复杂场景下,比如需要自行排版的文本、表格和图形类,光是将要渲染的数据计算出来,便容易面临性能瓶颈,而对于样式多样、结构复杂的内容来说,绘制过程也同样会出现性能瓶颈。
本文我们主要针对 Canvas 绘制的场景,来考虑将绘制的流程分为收集和渲染两部分。
很多时候,我们在后台数据库存储的只有图形的基本信息,对于需要排版计算的数据来说,则需要在拿到数据之后,再根据页面进行排版计算,完成后才能渲染到页面。
或许这样说会有些抽象,我们以表格的渲染为例来说明。
对于表格这样的产品来说,存储的往往是以单元格为基本单位的数据,如每个单元格的内容(可能是复杂的富文本、图片、图标结合)、样式(边框、背景色)、行列的宽高等。而在实际上页面渲染的时候,我们可能会根据行列宽高、每个单元格的边框线设置来绘制格子的布局。
除此之外,我们还可能需要考虑单元格内容是否会超出单元格,来判断是否需要截断渲染、是否需要换行显示等。这便要求我们需要对内容宽高进行测量,比如使用CanvasRenderingContext2D.measureText()
测量文本宽度,这依赖了浏览器环境下的 API 能力。这意味着我们无法在后台提前计算好这些数据,因此无法通过提前计算来加速渲染过程。
于是,我们需要在前端拿到后台数据后,再进行相应的排版计算。
如果说一边计算一边绘制,则整个过程的耗时会比较长,用户也可能会看到绘制过程,该体验不是很友好。因此,我们可以在计算过程中,先把要最终绘制的数据结果先收集到一起,Canvas 绘制的时候则可以直接用。
我们可以根据绘制内容,划分为以下的收集器和渲染器:
可见,收集器和渲染器的类型是一一对应的,渲染器在渲染的时候,可以直接从对应的收集器类型中获取数据,然后绘制。
还需要考虑一种情况,即相同的收集器和不同的收集器类型里,绘制内容有重叠时,需要考虑绘制堆叠的顺序。举个例子,单元格的文字需要在背景色上面,也就是说单元格的绘制需要比背景色要晚。
这意味着我们在收集的时候,还需要给收集的数据添加堆叠顺序,在绘制的时候,则按照堆叠顺序先后绘制。我们可以将收集器分为多个,每个收集器定义堆叠顺序,相同堆叠顺序的数据收集到一起。
其实,我们使用一个收集器,通过给数据添加渲染类型,来将不同类型的数据放在一起,方便统一管理。在渲染的时候,则先根据绘制类型和堆叠顺序进行排序,再进行绘制。
前面我们在《前端性能优化–Canvas 篇》一文中描述过,Canvas 上下文切换的成本消耗比较大,如果在复杂内容绘制的情况下,可能会导致性能问题。
使用收集器的一个好处是,我们可以将同类型同样式的渲染数据进行享元。对于样式完全一样的数据,收集器可通过对样式进行 hash 享元存储在一起。
这样绘制的时候,就可以将样式一样的内容一起绘制,减少 Canvas 上下文的切换。
举个简单的例子,假设线段的绘制支持颜色、粗细两种不同的样式,那么我们收集的时候可以将同颜色、同粗细的线段位置信息存储在一起:
1 | class LineCollector { |
通过这样的方式,我们将要绘制的数据收集起来,方便 Canvas 进行更高效的渲染。
通过将 Canvas 渲染过程拆分为收集和渲染两部分,架构上更清晰的同时,在一定程度上提升了渲染的性能和效率。而这样的架构设计,也更方便后续做更多的拓展,我会在后续篇章继续介绍。
]]>当我们想起来要考虑下自己未来工作方向的时候,常常是因为遇到了瓶颈。大多数人都是在遇到工作困境的时候,才会开始思考要怎么度过难关。但其实做工作规划最好的时候,就是在问题出现以前。
想要对自己的未来制定一些方案,得知道自己要去哪里。在团队里,可以根据自身喜好和团队的方向来找到自己在团队中的位置。但职业规划和团队中的定位不一样,首先我们要确定自己未来的发展方向。
对于程序员来说,未来的发展方向无非就几个:深挖技术领域、转型技术管理、转型其他类型管理、转行、考公务员等等。在前端领域,可以分为纯前端、全栈等方向,而纯前端和全栈也都各自有更加细分的领域,比如性能、渲染、动画绘制等等。
至于具体要选哪个方向,大都由个人偏好决定。一个人想要做什么事情,会同时受到很多因素的影响,除了自己对技术的热情和能力,还可能包括遇到的一些人和事,例如特别崇拜的某个开发、尊重的某位前辈、遇到过一些不公正的事情、或深感触动的一些事情,都可能会成为我们想要立志做某件事的契机。
很多时候,即使我们已经确认想要去往哪个方向,但实际上依然会被未来的某些事物改变。正如很多公司要求员工写 KPI,员工将 KPI 写得再详细,依然在半年后考核时需要重新修改,因为计划永远赶不上变化。但我们不能因为未来可能会变,就认为没有意义,也不去写 KPI。
这就好比我们在做习题本,本子的最后写好了答案,既然都知道自己最后都会看答案,那么我们做不做题、是否做错了都无关紧要了吗?显然不是的,做题是个需要思考计算的过程,通过最终的答案我们可以知道自己的思考方式是否有误、是否需要调整。写 KPI 也是一个道理,我们如果最初定的 KPI 与最终的不一样,是否需要去反思下为什么呢?KPI 本身的设计,不就是为了让我们确认自己规划做的事情,最终是否实现了吗?
因此,即使未来某个时候我们的职业方向会进行调整,此时此刻我们一样需要对此进行规划。通过规划我们才有了目标,有了目标,我们才会制定计划并为之努力实现。
其实我们每个人也都有各自擅长的部分,一般来说工作中更有效的方式是扬长避短,而不是花很多时间去补齐短板。我们常常会听到其他人说,“你没做过这些、要去挑战自己”,挑战自己的确是需要的,多尝试去做一些事情也总是好的。
如果我们需要在这场竞争中生存下来,必须拥有自己的不可替代性,也称之为技术壁垒。如果我们有擅长的领域,不妨尝试去深挖这样的领域,这样是一个相对容易的选择。
很多时候,也正是因为热爱和喜欢这些领域,我们才愿意投入更多的精力,因此在某种程度上也会更擅长。而做自己喜欢的事情的时候,即使再忙再苦再累,也一样会觉得很开心和值得。相反,如果做着不喜欢的工作时,每时每刻都是一种煎熬,每天上班就像去上坟。
可以的话,找一个可以发挥你自身优势的地方,那样的你会闪闪发光,你也会因此爱上你自己。
我刚进入互联网的时候,非科班出身、缺乏开发经验,自学了一周多就疯狂投简历。找工作很头疼,即使是裸辞的一番热血,再热烈也很容易被浇灭。最后虽然顺利找到了工作,但工资少得可怜。
那会从华为出来,待遇的落差总会不断地提醒我,到底是为了什么呢?但满怀的热情使得我每天上班充满干劲,下班后也继续在床上打着灯看书和写代码学习。那是 jQuery 横行的年代,似乎只需要掌握了它,你就能所向披靡。还有 CSS/CSS3 动画等,对 CSS 的掌握基本上是 10%的理解加上 90%的实践日积月累不断沉淀的。
那段时间可以感觉到成长很快,几位后台开发小哥哥带着入门,告诉我需要学习哪些知识、可以去哪些网站上学,然后就停不下来了。我曾经在面试的时候说自己学习能力很强,但是通常别人会问,你怎么证明呢?
不知道为什么,现在似乎大家多多少少都会不正视外包,“要不是能力不够也不会当外包”、“不能指望他们能学会什么”这样的话也经常会听到。可能是因为很多人的经历和体验里,都是比较顺畅阻力较少。而我也很荣幸曾经置于职业的低谷,使得很多时候能看到更多的可能性。
同样的,如今很多公司在招人的时候对学历的要求也越来越高,这是由于竞争市场资源过剩导致的,公司或许在性价比等方面考虑进行这样的调整,但我们不能自己限制住自己。我也见过一些特别厉害的开发,他们并不一定来自很好的学校。只是因为有着一股认真钻研的劲,他们看不到所谓的比较、竞争,专心致志地沉浸在自己的世界中,并做出了很多的成绩。
是否科班出身、是否外包出身、是否学历优秀,这些或许对其他人来说会有影响,但这些都不应该影响我们自身要去努力,不应该成为我们去给自身贴上一些标签的理由。
而如今大家都说什么寒冬到来、前端已死,要问我的想法,大概是世上无难事、只怕有心人罢了。
真正的热爱,从来不会因一份怎样的工作而受到影响,只不过如今大多数人更在意利益得失而已。
或许你会觉得制定计划不靠谱,因为事情永远都在变化。工作总是很复杂,不可控制的因素太多了。例如小明想要深入钻研浏览器渲染的方向,但实际上团队负责的业务都比较简单,因此小明常常需要快速上线某个新模块,节奏一直慢不下来,也完全没有机会和精力去研究自己的东西。
遇到这种情况,可以尝试将眼光再放长远一些。如果将职业规划比作一场马拉松,首先我们得确认目标是什么,是拿到前面的名次还是完成这场比赛?以前参加过中长跑的田径比赛,教练在比赛前会叮嘱我们几件事:
其实做职业规划和长跑很相似,我们需要确定一个较远的目标,然后控制好节奏、切忌着急和用力过猛。以我自己为例,一开始我想要往前端深度的领域发展,但我的起跑线是非科班+外包。显然,我无法一下子到达自己想去的地方,而此时自身的实力也无法和想要的岗位相匹配,因此我整体的职业路线是:外包公司前端 -> 中规模公司前端 -> 大公司重后台的业务部门前端 -> 大公司重前端业务部门前端。
这是一个经历了好些年的过程,每当我觉得在所在的团队中没法获得更多成长的时候,就会选择进入下一个阶段。如果目的很明确的话,你会清晰地知道自己什么时候该调整、接下来要去哪。
中间也遇到过一些团队,虽然团队中缺乏我想要的复杂大型前端业务项目,但团队会给道其他的机会,例如待遇上的回报、往管理方向发展带团队等等。很多条件都比较诱人,团队也的确给到了足够的诚意希望我留下来。但我知道自己想要的是前端领域的深挖,如果现在因为有其它诱惑而暂时选择放弃,以后遇到同样的困境是不是每次都会选择放弃呢?
因此,基本上我都选择了按照原目标继续往前走。有管理者觉得很疑惑,大家都往钱多的地方走,他问我是不是对钱没什么需要。我当时的回答是,把该学的知识和技能都掌握,如何赚钱它不该是我需要担心的问题。
将眼光放远一些,直到我们建立起自己的技术壁垒,在那之前即使挣到更多的钱、可以带团队干活,也依然可能会面临被这个行业抛弃。当然,这里的前提是我自身想要往技术的方向去发展。如果想要往管理方向去发展,一些带团队的经验也是很有帮助的。
如果现在的你距离自己的目标太远,那么不妨尝试找到去向最终目标的一个小目标,先往小目标去努力。通往目标的路上干扰很多,但如果你足够地坚定,总有一天也能去到自己想去的地方的。
对于每个技术人来说,都会遇到一些发展方向的选择困惑,比如“该往深度发展”还是“该往广度发展”。前端也不例外,我们的发展方向种类越来越多,但一个人的精力总是有限的,我们还是需要做出选择。
如今随着 Node.js 的普及,也有不少的前端开发慢慢转型做全栈、大前端等方向。
的确,对于有全栈工作经验的人来说,找工作的时候会更吃香。但我们日常工作中是否都有机会去接触后台开发、客户端开发这些内容呢?是否一定需要有这样的工作经验才能获得更好的发展呢?
很多时候,前端由于门槛较低,很多的前端开发(比如我)都不是计算机专业出身。我们对于计算机基础、网络基础、算法和数据结构等内容掌握很少,更多时候是这些知识的缺乏阻碍了我们在程序员这一职业的发展,这也是为什么很多前端开发苦恼自己到达天花板,想着转型全栈或者后台就能走得更远。
这其实是个误区。后台开发由于开发语言、服务器管理、计算机资源等工作内容的不一致,对于专业基础的要求更高,因此看上去似乎比前端能走得更远。但随着成熟的解决方案的出现,像分布式部署和管理、全链路跟踪等,以及运维和 DBA 等职位的出现、后台基本框架的完善,更多的后台开发技术选型的范围不大,在开发过程中也同样会偏向业务开发,因此更多的关注点会落在业务风险梳理、问题定位和追踪、业务稳定性、效率提升等地方。对于全栈开发中的后台开发,可能涉及的内容会更加局限一些。
所以,其实我们在日常工作中也可以更多地关注后台的实现和能力,除了可以更好地配合和理解后台的工作外,还可以提升自己对后台工作内容的理解。当然,最重要的依然是扎实地补充计算机基础知识。
全栈开发经验可能让我们更容易地找到工作,但只有基础知识的掌握足够深入,才可以在接触后台开发、终端开发等内容的时候,有足够的能力去快速高效地解决问题。
相比转型全栈,其实纯前端可深挖的方向也很多,包括关注性能的各种深入的性能优化领域、关注效能的脚手架/CI/CD 等构建领域、关注可维护的项目与代码设计等架构领域、关注浏览器渲染的游戏引擎/WebGL 等特殊领域。选择走广度方向会要求有足够丰富的项目经验,而选择走深度方向只需要在某个领域有足够深刻的理解和突破,就可以建立起稳固的技术壁垒。
对于前端同学来说,我们也常常会纠结与 ToB 和 ToC 的工作内容选择,它们之间区别多在于用户群体和数量。
一般来说,ToB 的业务服务于某一类用户群体,因此会根据服务对象的不一样,工作重点有所区别。例如,如果服务于银行,会对技术方案/安全性要求严格;如果服务于政府机构,则可能需要兼容较低版本的 IE 浏览器(笑),技术选型比较局限。但通常来说,ToB 业务的用户量并不会特别大,对性能要求较低,有些情况下也会由于机器部署环境封闭的原因,对网络和安全性要求较低,因此 ToB 业务可以更多关注开发效率提升、技术管理选型、项目可维护性等方面。
ToC 的业务用户量较大,对加载性能、浏览器兼容性等都要求很高,因此常常需要进行性能优化、兼容性检测、实时监控、SEO 优化等工作。
找工作的时候,拥有 ToC 业务开发经验通常会比拥有 ToB 业务开发经验的优势要大一点,因为 ToC 对前端的各个角度要求都相对较高。但在真正的工作中,由于精力和工作内容分配的问题,很多参与 ToC 业务的人更多只会关注到自己负责的模块部分,因此很多时候并没有掌握到较完整的 ToC 业务相关的关键技术方案。同样,即使是在做的是 ToB 业务,也有不少小伙伴会有很多时间去研究一些新技术、做很多的选型调研,也可以在这个过程中获得很好的成长。
所以,决定我们能否掌握更多的、成长更快的,或许业务的影响比我们想象中还要小,最终还是取决于自己。
当一份很赚钱但没什么技术含量的工作(下面成为工作 A),以及一份有趣又具备足够挑战性的工作(工作 B),这样两份工作放在我们面前的时候,大概很多人都会犹豫。这的确是一个很现实的问题,钱可以让我们买到很多自己想要的东西,但它却没法买来成长极快的工作经历。丰富的工作经历可以给我们带来竞争力,但短期内可能会带来经济上的困扰。
前面我有表达过自己的观点,如果我们掌握了足够的技术和能力,就不会担心自己赚不到钱。如果以这个角度来看,工作 B 显然会是我们的选择。但这个世界并不都是非黑即白的,如果我们身边大多数的小伙伴都选择了向钱看,他们每年赚到的钱甚至是我们的一两倍,大多数人都无法不为所动。当然,如果有一份工作又能成长又能赚钱的工作最好,但遇到这样的选择概率会很低。
我们可以将自己的职业发展分为几个阶段,然后针对每个阶段分析该阶段中最重要的一些目标,这些目标可以是自身能力的提升,也可以是工资的上涨,还可以是职级的晋升等等。那么,当我们遇到困扰的选择时,可以选择当前阶段中比较重要的目标相关的工作内容。
举个例子,张三家境不好,他希望毕业工作之后可以尽快帮家里还清贷款,那么张三可以先选择赚钱更多的工作 A。当贷款还清以后,张三可以进入下一个阶段,如果这个阶段他觉得自己要提升实力来维持在工作中的竞争力,那么他就可以选择成长更快的工作 B。
和张三不同,李四只想攒点钱买个房子,对他来说工作就只是一份工作。但如果他不提升自己的能力,或许就会面临被淘汰、也没法赚到足够的钱,因此李四可能需要在某个阶段选择更具挑战性的工作 B。
每个人的愿望都不一样,有些人希望攒一些积蓄、买个小房子、过点小日子,也有人希望在职场叱咤风云、留下自己的名字,还有人希望沉浸在自己的世界、一直钻研某个领域。我们的愿望决定了我们最终的目标,而为了实现这个目标,中间也可能会做些看起来与目标相背离的事情。
我们在做的事情到底有没有用、能不能到达想去的远方,这些只有时间能证明。很多时候,每个人看到的只有当下的片刻,我们不能因为现在眼里的自己不像自己,就感到不甘、难过、想要自暴自弃。同样也不能因为目前眼里其他人的趋利避害、趋于世俗而瞧不起对方。或许有些人我们无法理解也无法接受,但即使认为不是一路人,也该给予他们足够的尊重。
大多数人都存在这样的误区,认为做自己喜欢的事情就很幸运了,不应该再期望能赚到多少钱。
我也看到有些人为了追求自己想做的工作,愿意“不要工资”、“只要给我做的机会就好了”。做自己喜欢的事情固然很好,但这并不意味着我们必须要付出很多很多的代价。从公司的角度看,会因为一个人零成本而录用他的几率不大,更多的时候还是愿意付出更多的成本招聘一个有足够能力的人。如果想要争取一份工作,我们要做的是努力提升自己、让自己配得上这份工作的职责要求。
当然,也有人是因为想要转行,认为自己没有相关的工作经历,被录用的概率太低,认为如果自己不要钱对方可以给自己机会去学习就足够了。对于这种想法,我依然保持上述的建议:尽可能先提升自己的能力,通过自学也好,出去从有所关联的小职位、小公司开始做起也都是可以的。不需要提出不要工资这样的条件,这样只会更让对方认为你能力欠缺。
有些时候,我们还会陷入另外一个误区:如果因为做自己喜欢做的事情而获利,这种喜欢就不纯粹了。但谁能告诉我,为什么我们不能一边做自己喜欢的事情一边赚钱呢?如果我们做自己喜欢的事情,同时还能赚到钱,那不是会让自己更有动力、有更多的成本去继续做这些喜欢的事情吗?
]]>开发时间足够长时,我们常常会以项目的形式参与到具体的开发中,可能会负责项目的主导,或是作为核心开发负责某个模块、某个技术方案的落地。
在项目进行的每个阶段,我们都可以通过同样的方式去提升自己:
就像在代码开发前进行架构设计一样重要,我们在项目开始前,需要对项目的整个过程进行初步的预期,包括:
这么做有什么好处呢?如果不做方案调研和项目预期管理,那么对于项目过程中的风险则很难预测。这会导致项目的延期,甚至做到一般发现做不下去了。
在我们日常的工作中,这样的情况常常会遇到,很多人甚至对需求延期都已经习以为常了,认为需求能准时上线才是稀奇的事情。正因为大家都是这样的想法,我们更应该把这些事情做好来,这样才可以弯道超车。
首先,在项目开始的时候,需要进行工作量评估和分工排期。
进行工作量评估的过程可以分为三步:
当我们确认好技术方案之后,可以针对实现细节拆分具体的功能模块,分别进行工作量的预估和分工排期。具体的分工排期在多人协作的时候是必不可少的,否则可能面临分工不明确、接口协议未对齐就匆忙开工、最终因为各种问题而返工这些问题。
进行工作量评估的时候,可以精确到半天的工作量预期。对独自开发的项目来说,同样可以通过拆解功能模块这个过程,来思考具体的实现方式,也能提前发现一些可能存在的问题,并相应地进行规避。
提供完整的工作量评估和排期计划表(精确到具体的日期),可以帮助我们有计划地推进项目。在开发过程中,我们可以及时更新计划的执行情况,团队的其他人也可以了解我们的工作情况。
工作量评估和排期计划表的另外一个重要作用,是通过时间线去严格约束我们的工作效率、及时发现问题,并在项目结束后可针对时间维度进行项目复盘。
为了确保项目能按照预期进行,我们还要对可能存在的风险进行分析,提前做好对应的准备措施。
我们在项目开发过程中,经常会遇到这样的情况:
一个项目能按照预期计划进行,技术方案设计、分工和协作方式、依赖资源是否确定等,任何一各环节出现问题都可能导致整体的计划出现延误,这是我们不想出现的结果。
因此,我们需要主动把控各个环节的情况,及时推动和解决出现的一些多方协作的问题。
很多开发习惯了当代码开发完成、发布上线之后就结束了这个项目,其实他们遗漏了一个很重要的环节:复盘。
对于大多数开发来说,很多时候都不屑于主动邀功,觉得自己做了些什么老板肯定都看在眼里,写什么总结和复盘都是刷存在感的表现。实际上老板们每天的事情很多,根本没法关注到每一个人,我以前也曾经跟老板们问过这样一个问题:做和说到底哪个重要?
答案是两个都重要。把一件事做好是必须的,但将这件事分享出来,可以同样给团队带来更多的成长。
性能优化的工作可以用具体的耗时和 CPU 资源占用这些数据来做总结,工具的开发可以用接入使用的用户数量来说明效果,这种普普通通的项目上线,又该怎么表达呢?
我们可以用两个维度复盘:
其中,时间维度可以包括:
通过预期和最终结果的对比,我们可以直观看到是否存在延期等情况,分析原因分别是什么(比如方案设计问题、人员变动、协作方延期等)
如下图,假设项目分为一期、二期,我们可以在一期结束后,进行复盘分析并改进。同时还可以以时间线的方式对比开发时间结果:
除了时间维度以外,我们还可以通过衡量项目质量的方式来复盘,比如:
我们需要分析各个阶段存在的质量问题,并寻找原因(比如技术方案变更时考虑不全、设计稿还原度较低、自测时间不足等)。质量的维度同样可以用对比的方式来展示:
所以,为什么项目复盘很重要呢?
很多人会觉得做一个普通的前端项目,从开发到上线都没什么难度。一个字:“干”就完了。
实际上,项目的管理、推动和落地是工作中不可或缺的能力,这些不同于技术方案设计、代码编写,属于工作中的软技能。但正是这样的软技能会很大地影响我们的工作成果,也会影响自身的成长速度,是升职加薪的必备技能。
职场之所以让人不适,很多时候是由于它无法做到完美的公平。对于程序员来说,同样如此。
因此,为了能让自己付出的努力事半功倍,阶段性的输出是必不可少的。对于项目复盘来说,我们可以通过团队内外分享、邮件复盘总结等方式进行输出。
一般来说,可以通过几个方面来总结整理:
通过对项目进行复盘,除了可以让团队其他人和老板知道我们做了些什么,更重要的是,我们可以及时发现自身的一些问题并改进。
本文介绍了在项目开发过程中,要如何做好前期的准备,又该如何在项目结束后进行完整的复盘。
对于大部分前端开发来说,接触工具和框架开发、参与开源项目的机会比较少,很多时候我们写的都是“枯燥无聊”的业务代码。我们总认为只有做工具才会比较有意思、有技术挑战,很多时候会先入为主,认为业务代码写得再好也没用,也渐渐放弃了去思考要怎么把事情做好。
其实不只是工作中,我们生活里也可以常常进行反思和总结,这样我们的步伐才可以越跑越快。成长的过程中总会遇到各式各样的问题,有些问题被我们视而不见,有些问题我们选择了躲开,但其实我们还可以通过迎面应战、解决并反思的方式,在这样一次次战斗中快速地成长。
]]>本文将要介绍:
除了具体的前端领域知识以外,当我们开始负责起整个前端项目的管理时,需要具备一些方案选型、架构设计、项目瓶颈识别并解决等能力。
很多时候,我们的项目在刚搭建的时候规模会比较小,因此在项目启动阶段需要做简化,来保证项目能快速地上线。但从长期来看,一个项目还需要考虑到拓展性。换句话说,当项目开始变得较难维护的时候,我们就要进行一些架构或者流程上的调整。
在项目开始之前,我们需要做一系列的规划,像项目的定位(to B/C)、大小,像框架和工具的选型、项目和团队规范等,包括:
项目的维护永远是程序员的大头,多是“前人种树,后人乘凉”。但是很多时候,大家会为了一时的方便,对代码规范比较随意,就导致了我们经常看到有人讨论“继承来的代码”。
代码规范其实是团队合作中最重要的地方,使用一致的代码规范,会大大减少协作的时候被戳到的痛点。好的写码习惯很重要,包括友好的变量命名、适当的注释等,都会对代码的可读性有很大的提升。但是习惯是每个人都不一样,所以在此之上,我们需要有这样统一的代码规范。
一些工具可以很好地协助我们,像 Eslint 这样的工具,加上代码的打包工具、CI/CD 等流程的协助,可以把一些规范强行标准化,达到代码的统一性。还有像 prettier 这样的工具,可以自动在打包的时候帮我们进行代码规范的优化。
除了这些简单的命名规范、全等、单引双引等代码相关的规范,还有流程规范也一样重要。比如对代码进行 code review,尤其在改动公共库或是公共组件的时候。
最重要的还是多沟通。沟通是一个团队里必不可少、又很容易出问题的地方,我们要学会沟通和表达自己。
我们常常会觉得自己做的项目没有什么意思,每天都是重复的工作、繁琐的业务逻辑、糟糕的历史遗留代码,反观其他人都在做有技术、有难度、有挑战性的工作,越会难以喜欢上自己负责的工作。
实际上,那些会让我们觉得枯燥、反复、杂乱的工作内容,更是可以去改善做好、并能从中获得成长的地方。涉及前端工作的业务,只有极少一部分的业务例如涉及多人协同的在线文档、或是用户量很大的业务如电商、直播、游戏等,这些业务重心可能会稍微倾向前端,更多时候前端真的会处于编写页面、最多就用 Node.js 写写接入层等状况。好的业务可遇不可求,我们在遇到这些业务之前,就什么都不做了吗?
大多数工作中,对开发的要求都不仅限于实现功能。如果只是编写代码,刚毕业的应届生花几周时间也一样能做到,那么我们的优势在哪里呢?洞察工作中的瓶颈,并有足够的能力去设计方案、排期开发、解决并复盘,这些技能更能突显我们在岗位上的价值和能力。对团队来说,更需要这样能主动发现并解决问题的成员,而不是安排什么就只做什么的螺丝钉。
一般来说,用户量较大的项目的瓶颈通常会在兼容性、性能优化这些方面;对于一次性的活动页面,挑战点存在于如何高效地完成一次活动页面的开发或者配置,通常会使用配置系统、结合拖拽以及所见即所得等方式来生成页面;对于经常开发各式各样的管理端系统,优化方向则在于怎么通过脚手架快速地生成需要的项目代码、如何快速地发布上线等。我们要做的,就是找到工作中让自己觉得烦躁和不爽的地方,然后去改进优化它们。
找到项目的痛点或是瓶颈后,就需要设计相应的方案去解决它们。而当我们需要投入人力和时间成本去做一件事,就需要面临一个问题:如何让团队认同这件事情、并愿意给到资源让我们去完成它?
可以通过前期的调研,找一些业界相对成熟的方案作为参考。如果有多套方案,则需要对这些方案进行分析比较。例如,小明最近需要针对项目进行自动化性能测试能力的支持,因为项目规模大、模块多、参与开发的成员也有几十人,经常因为一些不同模块的变更导致项目的性能下降却没法及时发现问题,往往是等到用户反馈或是某次开发、产品或者测试发现的时候才得知。
不同于做工具和框架、参与开源协同,很多时候我们写的都是业务代码。我们总认为只有做工具才会比较有意思、也有技术挑战,但是业务代码就没有可以提升技术、挑战自己的地方了吗?其实并不是,很多时候我们先入为主、认为业务代码写得再好也没用、自己放弃了去做这样的事情。多多思考,你会发现每个项目都可以大有可为,你的未来也可以大不一样。
很多开发在进行编码实现功能的时候,都直接想到哪写到哪,也常常会出现代码写到一半发现写不下去,结果导致重新调整实现,最终项目从预期的一周变成了一个月、迟迟上线不了的问题。
当我们确认好技术方案之后,可以针对实现细节拆分具体的功能模块,分别进行工作量的预估和分工排期。这一步骤在多人协作的时候是必不可少的,否则可能面临分工不明确、接口未对齐就匆忙开工、最终因为各种问题而返工这些问题。而对单人项目来说,也可以通过拆解功能模块这个过程来思考具体的实现方式,也能提前发现一些可能存在的问题,并相应地进行规避。
提供完整的工作量评估和时间表,我们可以比较有计划地进行开发,同时团队的其他人也可以了解我们的工作情况,有时候大家能给到一些建议,也能避免对方不了解我们到底在做什么而导致的一些误会。而排期预估的另外一个重要作用,则是通过时间线去严格约束我们的工作效率、及时发现问题,以及项目结束后可针对时间维度进行项目复盘。
前面有说到,我们需要在参与项目的过程中具备 Owner 意识,即使这个项目并不是我们主导。风险把控则是作为 Owner 必须掌握的一个能力,我们需要确保项目能按照预期进行,则需要主动发现其中可能存在的风险并提前解决。
除了因为方案设计考虑不周而导致的一些返工风险,我们在项目进行过程中常常也会遇到依赖资源无法及时给到、依赖方因为种种原因无法按时支援、团队协作出现矛盾等各种问题,任何一块出现问题都可能导致整体的工期出现延误,这是我们不想出现的结果。因此,我们需要主动把控各个环节的情况,及时推动和解决出现的一些多方协作的问题。
通过前期准备的这些方案和工具,提前控制好一些可预见的风险,开发过程会更加顺利。但是如果我们的效果只有这些的话,很多时候是无法证明自己做了这么多事情的价值。那么,我们可以尝试用数据说话。
很多开发习惯了当代码开发完成、发布上线之后就结束了这个项目,其实他们遗漏了一个很重要的环节:复盘。通过复盘这种方式,我们可以发现自身的一些问题并改进,还可以让团队其他人以及管理者知道我们做了些什么,这是很重要的。
复盘的总结内容,可以通过邮件的方式发送给团队以及合作方,同时还可以作为自身的经验沉淀,后续更多项目中可以进行参考。如果使用得当,我们还可以通过这种方式来影响我们的团队和管理者,也是向上管理的一种方法。
但其实不只是工作中,我们生活里也可以常常进行反思和总结,这样我们的步伐才可以越跑越快。成长的过程中总会遇到各式各样的问题,有些问题被我们忽视而过,有些问题我们选择了逃避,但其实我们还可以通过迎面应战、解决并反思的方式,在这样一次次战斗中快速地成长。
每一个程序员都希望自己成为一个优秀的开发,实际上每个人对优秀的定义都不大一样。作为前端开发,除了专业能力以外,工作中还需要良好的表达与沟通能力。
如果我们还想继续往上走,通用计算机能力、架构能力、项目管理等能力也都需要提升。
]]>一般来说,技术方案的调研和设计过程可以分为几个阶段:
只有确保了技术方案的最优化、避免开发过程遇到问题需要推翻重做,从而能够快速落地并达成预期的效果。因此,在进行方案设计之前,对于项目存在的一些技术瓶颈、技术调整,我们需要先进行充分的前期调研。
在进行技术方案调研的时候,我们需要首先结合自身项目的背景、存在的痛点、现状问题进行分析,只有找到项目的问题在哪里,才可以更准确、彻底地去解决这些问题。
技术方案的设计很多时候并不是命题作文,更多时候我们需要自己去挖掘项目的痛点,然后才是提出解决方案。
很多前端开发常常觉得自己做的项目没什么意思,认为每天都是重复的工作、繁琐的业务逻辑、糟糕的历史遗留代码。
实际上,那些会让我们觉得枯燥和重复的工作内容,也是可以去改善做好、并能从中获得成长的地方。好的业务可遇不可求,如果工作内容跟自己的预期不一样,我们就什么都不做了吗?
我们可以主动寻找项目存在的问题和痛点,并尝试去解决。不同的项目或是同一个项目的不同时期,关注的技术点都会不一样。对于一个前端项目来说,技术价值常常体现在系统性能、稳定性、可维护性、效率提升等地方,比如:
找到项目的痛点之后,我们就可以进入项目的现状分析。
项目的痛点可以转化为一个目标方向,比如:
确定目标之后,我们就需要进行技术方案的设计,但很多时候由于项目现状存在的问题,一些技术优化的方案并不适用,需要进行方向的调整。
假设有一个同样规模大、成员多的小程序项目,由于该项目处于快速迭代的时期,考虑到投入产出比、产品形态也在不断调整,老板说“每个功能由开发自己保证”,决定不投入测试资源。
这意味着开发不仅需要在自测的时候确保核心用例的覆盖,同时也没有足够的排期来进行自动化测试(单元测试、集成测试、端到端测试等)的开发。
一般来说,我们还可以考虑建立用例录制和自动化回归的解决方案。比如开发一个浏览器插件,来获取用户操作的一些行为(比如 Redux 中的 Action 操作),将操作行为的页面结果(状态数据,比如 Redux 的 State)保存下来。在发布之前,可以通过自动化触发相同的操作行为,并与录制的页面结果进行比较,来进行回归测试。
但对于小程序的特殊性,我们无法让其运行在浏览器中,更无法获取到它的操作行为。在这样的情况下,还有什么办法可以保证系统的稳定性呢?
考虑到一个系统的上线过程包括开发、测试、灰度和发布四个阶段,如果无法通过测试阶段来及时发现问题,那么我们还可以通过灰度过程中来及时发现并解决问题。
比如,通过全埋点覆盖各个页面的功能,灰度过程中观察埋点曲线是否有异常、及时告警和排查问题、暂停灰度或者回滚等方式,来避免给更多的用户带来不好的体验。
通过灰度的方式来保证系统稳定性,会对局部的用户造成影响,这并不是一个最优的技术方案,它是考虑到项目的现状退而求其次的解决方案,但最终也同样可以达到提升系统稳定性这样一个目的。
当我们确定了技术优化的具体方向之后,便可以进行业界方案的调研阶段了。
当我们遇到一些技术问题并尝试解决的时候,需要提醒自己,这些问题肯定有其他人也遇到过。为了避免技术方案的设计过于局限,我们可以进行前期的调研,找一些业界相对成熟的方案作为参考,分析这些方案的优缺点、是否适用于自己的项目中。
我们可以通过几种方式去进行业界方案的调研:
举个例子,对于交互复杂、规模大型的应用,要如何管理各个模块间的依赖关系呢?业界相对成熟的解决方案是使用依赖注入体系,其中著名的开源项目中有 Angular 和 VsCode 都实现了依赖注入的框架,我们可以通过研究它们的相关代码,分析其中的思路以及实现方式。
开源项目源码很多,要怎么才能找到自己想看的部分呢?带着疑问有目的性地看,会简单轻松得多。比如上述的依赖注入框架,我们可以带着以下的问题进行研究:
通过这样的方式阅读源码,我们可以快速掌握自己需要的一些信息。在业界方案调研完成之后,我们需要结合自身项目进行具体的技术方案设计。
技术方案设计过程中,我们需要根据上述的调研资料进行整理,包括项目痛点、现状、业界方案等,然后进行方案的选型和对比,最终给到适合项目的解决方案。
业界的解决方案可能有多套,这时候我们需要对这些方案进行分析比较。
除此之外,如果需要投入人力和时间成本去做一件事,我们就会面临一个问题:如何让团队认同这件事情、并愿意给到资源让我去完成它呢?梳理项目现状和痛点、提供业界认可的案例参考、进行全面的方案对比和选型,也是一种方式。
例如,假设我们最近需要针对项目进行自动化性能测试能力的支持:
调研常见的一些性能分析方案,发现有几种方式:
其中,第一和第二种方案都无法从根本上解决遇到的问题。如果要彻底解决这个问题,可以考虑采取第三种方案,并打算通过自行研究分析 CDP(Chrome Devtools Protocol)生成的 JSON 来达到完全的自动化目的。
方案选型和对比是技术方案设计中重要的一个环节,可以将现状和痛点分析得更加全面,同时还可以避开一些其他人踩过的坑。
在大多数工作中,对开发的要求都不仅限于实现功能。你是否有想过,如果只是编写代码,刚毕业的应届生花几周时间也一样能做到,那么我们的优势在哪里呢?
洞察工作中的瓶颈,并有足够的能力去设计方案、排期开发、解决并复盘,这些技能更能突显我们在岗位上的价值和能力。对团队来说,更需要这样能主动发现并解决问题的成员,而不是安排什么就只做什么的螺丝钉。
技术的发展都离不开知识的沉淀、分享和相互学习,当我们遇到一些问题不知道该怎么解决的时候,可以试着站到巨人的肩膀上,说不定可以看到更多。
]]>《前端性能优化–归纳篇》中,我给大家介绍了很多常见的前端性能优化思路和方案,核心优化思想为时间上减少耗时、空间上降低资源占用。其中耗时优化在前端性能优化中更常见,优化方案包括网络请求优化、首屏加载优化、渲染过程优化、计算/逻辑运行提速四个方面。
性能优化通常需要投入不少的人力和成本来完成,因此更多的时候我们可以将其当作是一个项目的方式来进行管理。从项目管理的角度来讲,我们的性能优化工作会拆解为以下部分内容:
性能优化的第一步,就是要确定优化的目标和预期。在给出具体的数据之前,我们首先需要对一些性能数据进行定义,常见包括:
要选择合适有效的指标进行定义,比如由于前端框架的出现,Page Load 耗时(window.onload
事件触发的时间)已经难以用来作为页面可见时间的关键点,因此可以使用框架提供的生命周期,或者是使用 Largest Contentful Paint (LCP,关键内容加载的时间点)更为合适。
对需要关注的性能数据进行定义完成后,可以对它们进行目标和预期的确定,一般来说有两种方式:
在确定了目标和预期之后,我们便可以根据预期来确定优化的方向、技术方案。
根据确定的目标和预期,我们就可以选择合适的优化方案。
为什么不能将前面提到的全部技术方案都做一遍呢?显然这是不合理的。主要原因有两个:
举个例子,阿猪的预期目标是客户端内打开应用 TTI 耗时减少 30%,因此他可以选择的优化方案包括:
其中,5-6 需要客户端小伙伴进行支持,那么阿猪可以根据对方可以投入人力进行配合,来确定这两个优化点是否在本次方案中。
为了达成目标,对合适的技术优化点进行罗列之后,需要对每个优化点进行简单的调研,确定它们的优化效果。比如针对对首页数据进行分屏加载,可以通过简单的模拟测试,对比完整数据的 TTI 耗时,与首屏数据的 TTI 耗时,预估该技术点的优化效果如何。
最后,根据每个优化点的优化效果,以及相应的工作量评估,以预期为目标,选择性价比最优的技术方案。
在技术方案确定后,则需要对工作内容进行排期,并按计划执行。优化完成后,还需要结合目标和预期,对优化效果进行复盘,同时还可以提出未来优化的规划。
这个步骤主要是排期实现,耗时最多。一般来说,需要注意的有两点:
进行工作量评估的过程可以分为三步:
当我们确认好技术方案之后,可以针对实现细节拆分具体的功能模块,分别进行工作量的预估和分工排期。具体的分工排期在多人协作的时候是必不可少的,否则可能面临分工不明确、接口协议未对齐就匆忙开工、最终因为各种问题而返工这些问题。
进行工作量评估的时候,可以精确到半天的工作量预期。对独自开发的项目来说,同样可以通过拆解功能模块这个过程,来思考具体的实现方式,也能提前发现一些可能存在的问题,并相应地进行规避。
提供完整的工作量评估和排期计划表(精确到具体的日期),可以帮助我们有计划地推进项目。在开发过程中,我们可以及时更新计划的执行情况,团队的其他人也可以了解我们的工作情况。
工作量评估和排期计划表的另外一个重要作用,是通过时间线去严格约束我们的工作效率、及时发现问题,并在项目结束后可针对时间维度进行项目复盘。
我们在项目开发过程中,经常会遇到这样的情况:
一个项目能按照预期计划进行,技术方案设计、分工和协作方式、依赖资源是否确定等,任何一各环节出现问题都可能导致整体的计划出现延误,这是我们不想出现的结果。
因此,我们需要主动把控各个环节的情况,及时推动和解决出现的一些多方协作的问题。
很多开发习惯了当代码开发完成、发布上线之后就结束了这个项目,其实他们遗漏了一个很重要的环节:复盘。
我换过好多个团队,发现大多数团队和个人,都没有养成复盘的习惯。复盘是一个特别好的习惯,对于我们个人的成长也好,项目的优化和发展也好,都有很好的作用。
当然,也有一些人会把复盘当做背锅和甩锅,这是不对的。当我们在项目过程中,常常因为有 Deadline 而不断地赶节奏,大多数情况下都只能发现一个问题解决一个问题。而在项目结束之后,我们才可以跳出项目,做更加广视角下的回顾和思考。
有效的复盘,可以达到以下的效果:
对于大多数开发来说,很多时候都不屑于主动邀功,觉得自己做了些什么老板肯定都看在眼里,写什么总结和复盘都是刷存在感的表现。实际上老板们每天的事情很多,根本没法关注到每一个人,我以前也曾经跟老板们问过这样一个问题:做和说到底哪个重要?
答案是两个都重要。把一件事做好是必须的,但将这件事分享出来,可以同样给团队带来更多的成长。
通过对项目进行复盘,除了可以让团队其他人和老板知道我们做了些什么,更重要的是,我们可以及时发现自身的一些问题并改进。
项目复盘最好可以结合数据来说话,性能优化的工作可以用具体的耗时和 CPU 资源占用这些指标来做总结,工具的开发可以用接入使用的用户数量来说明效果。甚至是普普通通的项目上线,也都可以使用对比排期和实际开发,复盘各个环节的耗时和质量。
对于大部分前端开发来说,接触工具和框架开发、参与开源项目的机会比较少,很多时候我们写的都是“枯燥无聊”的业务代码。我们总认为只有做工具才会比较有意思、有技术挑战,很多时候会先入为主,认为业务代码写得再好也没用,也渐渐放弃了去思考要怎么把事情做好。
其实不只是工作中,我们生活里也可以常常进行反思和总结,这样我们的步伐才可以越跑越快。成长的过程中总会遇到各式各样的问题,有些问题被我们视而不见,有些问题我们选择了躲开,但其实我们还可以通过迎面应战、解决并反思的方式,在这样一次次战斗中快速地成长。
]]>我们常说的 SSR 指 Server-Side Rendering,即服务端渲染,属于首屏直出渲染的一种方案。
首先,我们来看一下 SSR 方案主要优化了哪些地方的性能。
一般来说,我们页面加载会分为好几个步骤:
现在大多数前端页面都是单页面应用,使用了一些前端框架来渲染页面,因此还会有以下的流程:
到这里,用户才完整地可见到当前页面的内容,并进行操作。可见,页面启动时的加载流程比较长,对应的耗时也都无法避免。
使用 SSR 服务端渲染,可以在第 1 步中直接返回当前页面的内容,浏览器可以直接进行渲染,再加载剩余的其他资源,因此优化效果是十分明显的。除了性能上的优化,SSR 还可以带来更好的 SEO 效果,因为搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
那一般来说 SSR 技术方案要怎么做呢?其实从上面的过程中,我们也可以推导出,需要根据页面路由和页面内容生成对应的 HTML 内容,用于首次获取 HTML 的时候直接返回。
现在我们大多数前端项目都会使用框架,而许多开源框架也提供了 SSR 能力。由于前端框架本身就负责动态拼接和渲染 HTML 的工作,因此实现 SSR 有天然的便利性。
以 Vue 为例子,Vue 提供了 vue-server-renderer 服务端能力,基本思想基本也是前面说过的:浏览器请求服务端时,服务端完成动态拼接 HTML 的能力,将拼接好的 HTML 直接返回给浏览器,浏览器可以直接渲染页面:
1 | // 省略,可直接查看官网例子:https://ssr.vuejs.org/zh/guide/#%E5%AE%8C%E6%95%B4%E5%AE%9E%E4%BE%8B%E4%BB%A3%E7%A0%81 |
当服务端收到请求时,生成 Vue 实例并依赖vue-server-renderer
的能力,将 Vue 实例生成最终的 HTML 内容。该例子中,服务端直接使用现有资源就可以完成直出 HTML 的拼接.
但是在更多的前端应用场景下,通常还需要服务端动态获取其他的数据,才能完整地拼接出首屏需要的内容。一般来说,我们可以在服务端接到浏览器请求时,同时获取对应的数据,使用这些数据完成 HTML 拼接后再返回给浏览器。
在 Vue SSR 能力中,可以依赖createApp
的能力,引入Vuex
提前获取对应的数据并更新到 Store 中(参考数据预取和状态),然后在服务端收到请求时,创建完整的 Vue 应用的能力:
1 | const createApp = require("/path/to/built-server-bundle.js"); |
前面我们讲到,Vue 提供了 SSR 的能力,这意味着我们可以使用 Vue 来完成客户端和服务端渲染,因此大部分的代码都可以复用。对于这种一份代码可分别在服务器和客户端上运行,我们成为“同构”。
对比自行实现 SSR 渲染,依赖开源框架提供的同构能力,一套代码可以分别实现 CSR 和 SSR,可大大节省维护成本。
还是以 Vue 为例,使用 Vue 框架实现同构,大概的逻辑如图:
不管是路由能力,还是组件渲染的能力,要保持同一套代码能分别运行在浏览器和服务端环境(Node.js)中,对于代码的编写则有一定的要求,比如 DOM 操作、window/document 对象等都需要谨慎,这些 Vue 官方指引也有介绍。
除此之外,服务端的入口逻辑显然会和客户端有差异,比如资源的获取方式、依赖的公共资源有所不一样等等。因此,在打包构建时会区分出两端的入口文件,并对通用逻辑做整合打包。这些内容也都在上面的图中有所体现。
如果我们并没有强依赖前端框架,或是我们的项目过于复杂,此时可能要实现同构需要的成本比较大(抽离通用模块、移除环境依赖代码等)。考虑到项目的确需要 SSR 来加速页面可见,此时我们可以针对首屏渲染内容,自行实现 SSR 渲染。
SSR 核心思想前面也讲过好几遍了,因此要做的事情也比较明确:根据不同的路由,提供对于的页面首屏拼接的能力。由于不强依赖于同构,因此可以直接使用其他语言或是 ejs 来实现首屏 HTML 内容的拼接。
显然,非同构的方案实现 SSR 的成本,比同构的方案成本要高不少,并且还存在代码一致性、可维护性等一系列问题。因此,即使首屏直出的内容无法使用框架同构,大多数情况下,我们也会考虑尽量复用现有的代码,抽离核心的通用代码,并提供 SSR 服务代码编译打包的能力。
举个例子,假设我们的页面完全由 Canvas 进行渲染,显然 Canvas 是无法直出的。但正因为 Canvas 渲染前,需要加载的代码、计算渲染内容等各种流程过长,耗时较多,想要实现 SSR 渲染则可能只能考虑,针对首屏内容做一套 DOM/SVG 渲染用于 SSR。
基于这样的情况下,我们需要尽量复用计算部分的能力,抽离出通用的 Canvas/DOM/SVG 渲染接口,以尽可能实现对接口编程而不是对实现编程。
上面主要围绕 SSR 的实现思想,介绍了开源框架 SSR、同构/非同构等 SSR 方案。
其实除了代码实现的部分以外,一个完整的 SSR 方案,还需要考虑:
我们在选择一个技术方案的时候,不能只看它能带来什么收益,同时还需要评估一并带来的风险以及弊端。
对于 SSR 来说,收益是显而易见的,前面也有提到:
而其弊端也是客观存在的,包括:
对于最后一点,有时候也会被我们忽略。因为 SSR 在最开始就提供了首屏完整的 HTML 内容,用户可见时间极大地提前了,我们常常会忘了关注页面所有功能加载完成、页面可交互的时间点。显然,由于浏览器需要在首屏时渲染完整的 HTML 内容,该过程也是需要一定的耗时的,所以后面的其他步骤完成的时间点都会有所延迟。如果首屏 HTML 内容很多/复杂的情况下,这种情况会更明显。
SSR 的内容大概讲到这里,其实在更多的时候,SSR 方案的重点往往是文中一笔带过的弊端。实现一套同构渲染的代码,亦或是维护两套分别用于 CSR/SSR 的代码,这些方案的目的和方向都比较明确。
而 SSR 部署在什么环境、使用服务端还是 Serverless 生成,是否结合缓存实现、缓存更新策略又该是怎样的,如何保证非同构代码的渲染一致性,这些问题才是我们在将 SSR 方案落地过程中,需要反复思考和琢磨的问题。
我们在做方案调研的时候,也常常会过于关注开发成本和最终效果,从而忽略了整个项目和方案过程中的许多可能性。虽然目的的确很重要,但要记住过程也是很重要的。
]]>今年来,听到了许多事、也看到了许多,看着不少认识的小伙伴一个个离开,万分感慨。
老板们强调着互联网寒冬,希望每个人都努力卷起来,最好能“当成自己的事业”来奋斗。因为冰河世纪,裁掉了好一部分人;因为冰河世纪,留下的每个人分到的事情更多;还因为冰河世纪,待遇都有所降低。
刚开始,不少人为了能留下来,的确变得更卷了。时间长了后,大部分人还是逐渐恢复了原本的节奏。毕竟要马跑,也得让马吃点草。
不少离开的人反而有了更好的去处,逃离了压抑的工作环境,同时还拿到了更好的待遇。这么一对比,其实有时候被迫脱离舒适的环境,其实也未必是件坏事,反而是帮我们下决心了。
其实在所谓的互联网寒冬以前,我都十分重视个人的成长,因为温水煮青蛙是一件很危险的事情。不过相比于担心被淘汰,更多还是出于对自身的要求吧,我也挺喜欢不断成长的滋味的。
我一直认为,有能力的人走到哪里都不会担心。能力提升上去了,不会担心被淘汰,即使离开了也能很快找到下一份工作。因为有能力的人,哪里都缺。
在平时的工作里,其实的确能看到不少问题。这些问题我在之前的文章中也有所提过,比如工作方式是否过于流水线完成任务,比如是否有给自己留下足够的时间来思考和总结,比如是否过于关注得失而忽略了自身真正的成长,等等。
还是那句话,忙并不一定能有所成长,你需要花点时间偶尔复盘一下。
事情做得好不好,这也和不同的领导风格有关。有些老板喜欢给你安排事情的、不喜欢自作主张的,也有些老板喜欢你主动思考和提出更多解决方案的。
看来,打铁得自身硬的同时,遇到一个合得来的老板也挺重要的。
当然,这并不是说被淘汰的都是能力不够的人。相反,我看到许多有潜力有能力的人离开了。除了个人选择之外,大部分原因便是归咎于其“影响力不够”。
其实我是十分讨厌影响力这个词的,因为它简陋又粗暴地描述大家的工作成果。是因为大家的工作成果无法被有效地量化,也无法确切地找出其中的问题,才会常常使用“影响力不够”这样的词语来概况总结。
但工作成果无法量化,更多时候是团队管理存在的问题。钻了空子的人,便会常常“刷脸”来提升自己的存在感,而遗憾的是,这样的操作常常会带来一定的效果。许多埋头苦干的人被忽略了,因为他们发声更少。
现实是,虽然我很讨厌影响力这种话,但实际上它常常就会在耳边响起。我也看到不少的小伙伴因为这种所谓的“影响力”在各种事情上被影响。因此,我还是建议大家,该表达的时候就要发声,这个浮躁的社会不会在乎你真正做了多少,他们只在乎他们看到了多少。
我不倡导过度地刷脸和表达,因此比较简单做到的是:发现问题积极响应、解决进展及时同步、风险及时同步。除此之外,自己的一些工作相关的想法其实也完全可以分享,比如提升工作效率的小技巧、解决某种问题的小技巧等等。
其实有时候会想,这行业的领导还挺好当的。当然,好的老板很难当,但只是个老板的话,感觉还挺简单的,反正事情做好了都归功老板,出问题了底下人背锅。即使是在所谓的冰河世纪,裁员也会先裁底下员工,就算裁到 leader,也可以拿着大家做的成果出去轻松找到下一份工作。
虽然他们也常常说 leader 不好当,压力大事情多。但如果真的只是徒增压力和责任,没有其他收益,大概也不会那么多人争破头去抢这样的位置了吧。
]]>