<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Here. There.</title>
  
  <subtitle>Love ice cream. Love sunshine. Love life. Love the world. Love myself. Love you.</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://godbasin.github.io/"/>
  <updated>2023-07-19T03:08:27.277Z</updated>
  <id>https://godbasin.github.io/</id>
  
  <author>
    <name>被删</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>复杂渲染引擎架构与设计--3.底层渲染适配</title>
    <link href="https://godbasin.github.io/2023/07/19/render-engine-bottom-render-architecture/"/>
    <id>https://godbasin.github.io/2023/07/19/render-engine-bottom-render-architecture/</id>
    <published>2023-07-19T03:07:52.000Z</published>
    <updated>2023-07-19T03:08:27.277Z</updated>
    
    <content type="html"><![CDATA[<p>前面我们介绍了复杂渲染引擎中，使用的收集和渲染、以及插件等架构设计。至于底层具体的绘制实现，前面提到的多是 Canvas，实际上我们还可以适配不同的绘制引擎。</p><a id="more"></a><h2 id="多渲染方式适配"><a href="#多渲染方式适配" class="headerlink" title="多渲染方式适配"></a>多渲染方式适配</h2><p>关于渲染引擎整体架构和插件架构的设计，已在<a href="https://godbasin.github.io/front-end-playground/front-end-basic/render-engine/render-engine-render-and-collect.html">《收集与渲染》</a>、<a href="https://godbasin.github.io/front-end-playground/front-end-basic/render-engine/render-engine-plugin-design.html">《插件的实现》</a>两篇文章中介绍过，渲染引擎架构如图：</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-engine-plugin-design-1.jpg" alt></p><p>底层渲染引擎由收集器和渲染器组成，其中收集器收集需要渲染的渲染数据，渲染器则负责将收集到的数据进行直接渲染。</p><p>本文我们将会介绍渲染器的多种渲染方式的适配，其中常见的就包括：</p><ul><li>Canvas 渲染</li><li>SVG 渲染</li><li>DOM 渲染</li><li>其他渲染方式（如 WEBGL 渲染等）</li></ul><h3 id="适配架构设计"><a href="#适配架构设计" class="headerlink" title="适配架构设计"></a>适配架构设计</h3><p>对于多种渲染方式的适配，架构设计上还比较简单：</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-engine-bottom-render-architecture-1.jpg" alt></p><p>从收集器收集到的数据，通过适配的方式，转换成不同的绘制结果。举个例子，同样是一个单元格内容：</p><ul><li>Canvas 将需要分别绘制单元格背景（矩形）、单元格边框（线段）、单元格内容（文本）</li><li>SVG 与 Canvas 相似，但 SVG 需要注意元素的层级关系，组合成单元格</li><li>DOM 则可以通过一个<code>&lt;div&gt;</code>元素或是<code>&lt;table&gt;/&lt;tr&gt;/&lt;td&gt;</code>等表格元素来绘制</li></ul><p>一般来说，我们如果使用多种渲染方式，还需要考虑渲染一致性。渲染一致性是指，使用 Canvas 绘制的结果，需要与 SVG、DOM 绘制渲染的结果保持一致，不能出现太大的跳动或是位置、样式不一致的结果。</p><p>因此，我们在进行渲染的时候，根据选择的渲染方式，还需要做不同的兼容适配。</p><p>以上几种渲染方式中，DOM 渲染会受浏览器自身的排版引擎影响，这种影响可能是正面的，也可能是负面的。比如，我们 Canvas 排版方式是尽量接近浏览器原生的方式，那么当我们适配 DOM 渲染的时候则比较省力气。但如果说像在线表格这种场景，使用 DOM 进行表格的排版，则可能会遇到比较多的问题。</p><p>举个例子，我们都知道 DOM 里的表格元素（<code>&lt;table&gt;</code>/<code>&lt;tr&gt;</code>/<code>&lt;td&gt;</code>等）是最难驾驭的，因为浏览器对它们的处理总是在意料之外，宽高难以控制意味着我们将很难将其与 Canvas/SVG 的渲染效果对齐。因此，我们很可能需要在表格元素里嵌套绝对定位的<code>&lt;div&gt;</code>元素，来使得表格最终渲染不会被轻易撑开导致偏差。</p><p>除此之外，我们还需要注意文字的排版、换行等情况在 Canvas/SVG 和 DOM 渲染中需要尽量保持一致。</p><h3 id="各种渲染方式的选择"><a href="#各种渲染方式的选择" class="headerlink" title="各种渲染方式的选择"></a>各种渲染方式的选择</h3><p>每种渲染方式都有各自的优缺点。</p><p>在图表渲染引擎中，最常见的是 Canvas 渲染和 SVG 渲染，我们也可以从 ECharts 官网中找到两者的对比描述：</p><blockquote><ol><li>一般来说，Canvas 更适合绘制图形元素数量较多（这一般是由数据量大导致）的图表（如热力图、地理坐标系或平行坐标系上的大规模线图或散点图等），也利于实现某些视觉特效。</li><li>但在不少场景中，SVG 具有重要的优势：它的内存占用更低（这对移动端尤其重要）、并且用户使用浏览器内置的缩放功能时不会模糊。</li></ol><p>选择哪种渲染器，可以根据软硬件环境、数据量、功能需求综合考虑：</p><ul><li>在软硬件环境较好，数据量不大的场景下，两种渲染器都可以适用，并不需要太多纠结</li><li>在环境较差，出现性能问题需要优化的场景下，可以通过试验来确定使用哪种渲染器。比如：<ul><li>在需要创建很多 ECharts 实例且浏览器易崩溃的情况下（可能是因为 Canvas 数量多导致内存占用超出手机承受能力），可以使用 SVG 渲染器来进行改善</li><li>如果图表运行在低端安卓机，或者我们在使用一些特定图表如水球图等，SVG 渲染器可能效果更好</li><li>数据量较大（经验判断 &gt; 1k）、较多交互时，建议选择 Canvas 渲染器</li></ul></li></ul></blockquote><p>而在在线表格的场景，我们会发现不同的团队会选择不同的渲染方式：</p><ul><li>谷歌表格使用了 Canvas/DOM 两种渲染方式，其中 DOM 渲染主要用于首屏直出</li><li>金山表格使用了 Canvas/SVG 两种渲染方式，其中 SVG 渲染主要用于首屏直出</li><li>飞书表格使用了 Canvas 渲染</li></ul><p>其实我们可以发现，这些团队很多在使用几种渲染方式，原因几乎都是因为使用了 Canvas 绘制作为主要渲染方式。但考虑到首屏渲染的情况，Canvas 则需要一系列的数据计算和渲染过程，不适合首屏直出的方式，因此会适配上 DOM 或者 SVG 进行首屏直出。</p><p>实际上，Canvas 渲染有一个比较致命的弱点：交互性很差。比如用户选择某个格子，进行拖拽、调整宽高、右键菜单等操作，在 Canvas 上是很难命中具体的元素的。因为 Canvas 绘制过程中并不像 DOM 和 SVG 一样有层次结构，最终的渲染结果也只是一个图像。因此，在线表格场景下大多数 Canvas 绘制都需要结合 DOM 引擎一起，获取到用户选择的元素、处理用户交互事件，然后进行二次计算和响应。</p><p>关于首屏直出，后面有空也可以简单唠唠。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>本文介绍了渲染引擎架构中，使用多种渲染方式以及底层渲染器适配的设计。</p><p>我们常常说给项目选择最优的解决方案，实际上我们也会发现，正因为往往没有所谓最优解，这些产品才会针对不同的场景下提供了不同的解决办法。比如，考虑到性能问题 ECharts 提供了 Canvas/SVG 两种绘制方式；又比如考虑到首屏直出的效率，各个在线表格的团队分别适配了更合适的渲染方式。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;前面我们介绍了复杂渲染引擎中，使用的收集和渲染、以及插件等架构设计。至于底层具体的绘制实现，前面提到的多是 Canvas，实际上我们还可以适配不同的绘制引擎。&lt;/p&gt;
    
    </summary>
    
      <category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
    
    
      <category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>大型前端项目的常见问题和解决方案</title>
    <link href="https://godbasin.github.io/2023/07/01/complex-front-end-project-solution/"/>
    <id>https://godbasin.github.io/2023/07/01/complex-front-end-project-solution/</id>
    <published>2023-07-01T11:38:33.000Z</published>
    <updated>2023-07-01T11:39:36.532Z</updated>
    
    <content type="html"><![CDATA[<p>或许你会感到疑惑，怎样的项目算是大型前端项目呢？我自己的理解是，项目的开发人员数量较多（10 人以上？）、项目模块数量/代码量较多的项目，都可以理解为大型前端项目了。</p><p>在前端业务领域中，除了大型开源项目（热门框架、VsCode、Atom 等）以外，协同编辑类应用（比如在线文档）、复杂交互类应用（比如大型游戏）等，都可以称得上是大型前端项目。对于这样的大型前端项目，我们在开发中常常遇到的问题包括：</p><ol><li>项目代码量大，不管是编译、构建，还是浏览器加载，耗时都较多、性能也较差。</li><li>各个模块间耦合严重，功能开发、技术优化、重构工作等均难以开展。</li><li>项目交互逻辑复杂，问题定位、BUG 修复等过程效率很低，需要耗费不少精力。</li><li>项目规模太大，每个人只了解其中一部分，需求改动到不熟悉的模块时常常出问题。</li></ol><p>其实大家也能看到，大型前端项目中主要的问题便是“管理混乱”。所以我个人觉得，对于代码管理得很混乱的项目，你也可以认为是“大型”前端项目（笑）。</p><h2 id="问题-1：项目代码量过大"><a href="#问题-1：项目代码量过大" class="headerlink" title="问题 1：项目代码量过大"></a>问题 1：项目代码量过大</h2><p>对于代码量过大（比如高达 30W 行）的项目，如果不做任何优化直接全量跑在浏览器中，不管是加载耗时增加导致用户等待时间过久，还是内存占用过高导致用户交互卡顿，都会给用户带来不好的体验。</p><p>性能优化的解决方案在<a href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/">《前端性能优化–归纳篇》</a>一文中也有介绍。其中，对于代码量、文件过多这样的性能优化，可以总结为两个字：</p><ul><li><strong>拆</strong>：拆模块、拆公共库、拆组件库</li><li><strong>分</strong>：分流程、分步骤</li></ul><p>项目代码量过大不仅仅会影响用户体验，对于开发来说，代码开发过程中同样存在糟糕的体验：由于代码量过大，开发的本地构建、编译都变得很慢，甚至去打水 + 上厕所回来之后，代码还没编译完。</p><p>从维护角度来看，一个项目的代码量过大，对开发、编译、构建、部署、发布流程都会同样带来不少的压力。因此除了浏览器加载过程中的代码拆分，对项目代码也可以进行拆分，一般来说有两种方式：</p><p><strong>1. multirepo，多仓库模块管理，通过工作流从各个仓库拉取代码并进行编译、打包</strong>。</p><ul><li>优点：模块可根据需要灵活选择各自的编译、构建工具；每个仓库的代码量较小，方便维护</li><li>缺点：项目代码分散在各个仓库，问题定位困难（使用<code>npm link</code>有奇效）；模块变动后，需要更新相关仓库的依赖配置（使用一致的版本控制和管理方式可减少这样的问题）</li></ul><p><strong>2. monorepo，单仓库模块管理，可使用 lerna 进行包管理</strong>。</p><ul><li>优点：项目代码可集中进行管理，使用统一的构建工具；模块间调试方便、问题定位和修复相对容易</li><li>缺点：仓库体积大，对构建工具和机器性能要求较高；对项目文件结构和管理、代码可测试和维护性要求较高；为了保证代码质量，对版本控制和 Git 工作流要求更高</li></ul><p>两种包管理模式各有优劣，一般来说一个项目只会采用其中一种，但也可以根据具体需要进行调整，比如统一的 UI 组件库进行分仓库管理、核心业务逻辑在主仓库内进行拆包管理。</p><blockquote><p>题外话：很多人常常在争论到底是单仓好还是多仓好，个人认为只要能解决开发实际痛点的仓，都是好仓，有时候过多的理论也需要实践来验证。</p></blockquote><h2 id="问题-2：模块耦合严重"><a href="#问题-2：模块耦合严重" class="headerlink" title="问题 2：模块耦合严重"></a>问题 2：模块耦合严重</h2><p>不同的模块需要进行分工和配合，因此相互之间必然会产生耦合。在大型项目中，由于模块数量很多（很多时候也是因为代码量过多），常常会遇到模块耦合过于严重的问题：</p><ol><li>模块职责和边界定义不清晰，导致模糊的工作可能存在多个模块内。</li><li>各个模块没有统一管理，导致模块在状态变更时需要手动通知相关模块。</li><li>模块间的通信方式设计不合理，导致全局事件满天飞、A 模块内直接调用 B 模块等问题，隐藏的引用和事件可能导致内存泄露。</li></ol><p>对于模块耦合严重的模块，常见的解耦方案比如：</p><ul><li>使用事件驱动的方式，通过事件来进行模块间通信</li><li>使用依赖倒置进行依赖解耦</li></ul><h3 id="事件驱动进行模块解耦"><a href="#事件驱动进行模块解耦" class="headerlink" title="事件驱动进行模块解耦"></a>事件驱动进行模块解耦</h3><p>使用事件驱动的方式，可以快速又简单地实现模块间的解耦，但它常常又带来了更多的问题，比如：</p><ul><li>全局事件满天飞，不知道某个事件来自哪里，被多少地方监听了</li><li>无法进行事件订阅的销毁管理，容易存在内存泄露的问题</li><li>事件维护困难，增加和调整参数影响面广，容易触发 bug</li></ul><h3 id="依赖倒置进行模块解耦"><a href="#依赖倒置进行模块解耦" class="headerlink" title="依赖倒置进行模块解耦"></a>依赖倒置进行模块解耦</h3><p>我们还可以使用依赖倒置进行依赖解耦。依赖倒置原则有两个，包括：</p><ol><li>高层次的模块不应该依赖于低层次的模块，两者都应该依赖于抽象接口。</li><li>抽象接口不应该依赖于具体实现，而具体实现则应该依赖于抽象接口。</li></ol><p>使用以上方式进行设计的模块，不会依赖具体的模块和细节，只按照约定依赖抽象的接口。</p><p>如果项目中有完善的依赖注入框架，则可以使用项目中的依赖注入体系，像 Angular 框架便自带依赖注入体系。依赖注入在大型项目中比较常见，对于各个模块间的依赖关系管理很实用，比如 VsCode 中就有使用到依赖注入。</p><h3 id="VsCode：结合事件驱动与依赖倒置进行模块解耦"><a href="#VsCode：结合事件驱动与依赖倒置进行模块解耦" class="headerlink" title="VsCode：结合事件驱动与依赖倒置进行模块解耦"></a>VsCode：结合事件驱动与依赖倒置进行模块解耦</h3><p>在 VsCode 中，我们也可以看到使用了依赖注入框架和标准化的<code>Event/Emitter</code>事件监听的方式，来对各个模块进行解耦（可参考<a href="https://godbasin.github.io/front-end-playground/front-end-basic/deep-learning/vscode-event.html">《VSCode 源码解读：事件系统设计》</a>）：</p><ul><li>各个模块的生命周期（初始化、销毁）统一由框架进行管理：通过提供通用类<code>Disposable</code>，统一管理相关资源的注册和销毁</li><li>模块间不直接引入和调用，而是通过声明依赖的方式，从框架中获取相应的服务并使用</li><li>不直接使用全局事件进行通信，而是通过订阅具体服务的方式来处理：通过使用同样的方式<code>this._register()</code>注册事件和订阅事件，将事件相关资源的处理统一挂载到<code>dispose()</code>方法中</li></ul><p>使用依赖注入框架的好处在于，各个模块之间不会再有直接联系。模块以服务的方式进行注册，通过声明依赖的方式来获取需要使用的服务，框架会对模块间依赖关系进行分析，判断某个服务是否需要初始化和销毁，从而避免了不必要的服务被加载。</p><p>在对模块进行了解耦之后，每个模块都可以专注于自身的功能开发、技术优化，甚至可以在保持对外接口不变的情况下，进行模块重构。</p><p>实际上，在进行代码编程过程中，有许多设计模式和理念可以参考，其中有不少的内容对于解耦模块间的依赖很有帮助，比如接口隔离原则、最少的知识原则/迪米特原则等。</p><p>除了解决问题，还要思考如何避免问题的发生。对于模块耦合严重这个问题，要怎么避免出现这样的情况呢？其实很依赖项目管理的主动意识和规范落地，比如：</p><ol><li>项目规模调整后，对现有架构设计进行分析，如果不再合适则需要进行及时的调整和优化。</li><li>使用模块解耦的技术方案，将各个模块统一交由框架处理。</li><li>梳理各个模块的职责，明确每个模块负责的工作和提供的功能，确定各个模块间的边界和调用方式。</li></ol><h2 id="问题-3：问题定位效率低"><a href="#问题-3：问题定位效率低" class="headerlink" title="问题 3：问题定位效率低"></a>问题 3：问题定位效率低</h2><p>在对模块进行拆分和解耦、使用了模块负责人机制、进行包拆分管理之后，虽然开发同学可以更加专注于自身负责模块的开发和维护，但有些时候依然无法避免地要接触到其它模块。</p><p>对于这样大型的项目，维护过程（熟悉代码、定位问题、性能优化等）由于代码量太多、各个函数的调用链路太长，以及函数执行情况黑盒等问题，导致问题定位异常困难。要是遇到代码稍微复杂点，比如事件反复横跳的，即使使用断点调试也能看到眼花，蒸汽眼罩都得多买一些（真的贵啊）。</p><p>对于这些问题，其实可以有两个优化方式：</p><ol><li>维护模块指引文档，方便新人熟悉现有逻辑。文档主要介绍每个模块的职责、设计、相关需求，以及如何调试、场景的坑等。</li><li>尝试将问题定位过程进行自动化实现，比如模块负责人对自身模块执行的关键点进行标记（使用日志或者特定的断点工具），其他开发可根据日志或是通过开启断点的方式来直接定位问题</li></ol><p>这个过程，其实是将模块负责人的知识通过工具的方式授予其他开发，大家可以快速找到某个模块经常出问题的地方、模块执行的关键点，根据建议和提示进行问题定位，可极大地提升问题定位的效率。</p><p>除了问题定位以外，各个模块和函数的调用关系、调用耗时也可以作为系统功能和性能是否有异常的参考。之前这块我也有简单研究过，可以参考<a href="https://godbasin.github.io/2020/06/21/trace-stash/">《大型前端项目要怎么跟踪和分析函数调用链》</a>。</p><p>因此，我们还可以通过将调用堆栈收集过程自动化、接入流水线，在每次发布前合入代码时执行相关的任务，对比以往的数据进行分析，生成系统性能和功能的风险报告，提前在发布前发现风险。</p><h2 id="问题-4：项目复杂熟悉成本过高"><a href="#问题-4：项目复杂熟悉成本过高" class="headerlink" title="问题 4：项目复杂熟悉成本过高"></a>问题 4：项目复杂熟悉成本过高</h2><p>即使在项目代码量大、项目模块过多、耦合严重的情况下，项目还在不断地进行迭代和优化。遇到这样的项目，基本上没有一个人能熟悉所有模块的所有细节，这会带来一些问题：</p><ul><li>对于新需求、新功能，开发无法完整地评估技术方案是否可以实现、会不会带来新的问题</li><li>需求开发时需要改动不熟悉的代码，无法评估是否存在风险</li><li>架构级别的优化工作，难以确定是否可以真正落地</li><li>一些模块遗留的历史债务，由于工作进行过多次交接，相关逻辑已无人熟悉，无法进行处理</li></ul><p>导致这些问题的根本原因有两个：</p><ul><li>开发无法专注于某个模块开发</li><li>同一个模块可能被多个人调整和变更</li></ul><p>对于这种情况，可以使用模块负责人的机制来对模块进行所有权分配，进行管理和维护：</p><ol><li>每个开发都认领（或分配）一个或多个模块，并要求完全熟悉和掌握模块的细节，且维护文档进行说明。</li><li>对于需求开发、BUG 修复、技术优化过程中涉及到非自身的模块，需要找到对应模块的负责人进行风险评估和代码 Review。</li><li>模块的负责人负责自身模块的技术优化方案，包括性能优化、自动化测试覆盖、代码规范调整等工作。</li><li>对于较核心/复杂的模块，可由多个负责人共同维护，协商技术细节。</li></ol><p>通过模块负责人机制，每个模块都有了对应的开发进行维护和优化，开发也可以专注于自身的某些模块进行功能开发。在人员离职和工作内容交接的时候，也可以通过文档 + 负责人权限的方式进行模块交接。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>大型项目的这些痛点，其实只是我们工作中痛点的缩影。技术上能解决的问题都是小事，管理和沟通上的事情才更让人头疼。</p><p>除此之外，在我们的日常工作中，通常也会局限于某块功能的实现和某个领域的开发。如果这些内容并没有足够的深度可以挖掘，对个人的成长发展也可能会有限制。在这种情况下，我们还可以主动去了解和学习其它领域的知识，也可以主动承担起更多的工作内容。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;或许你会感到疑惑，怎样的项目算是大型前端项目呢？我自己的理解是，项目的开发人员数量较多（10 人以上？）、项目模块数量/代码量较多的项目，都可以理解为大型前端项目了。&lt;/p&gt;
&lt;p&gt;在前端业务领域中，除了大型开源项目（热门框架、VsCode、Atom 等）以外，协同编辑类应
      
    
    </summary>
    
      <category term="前端技能提升" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E6%8A%80%E8%83%BD%E6%8F%90%E5%8D%87/"/>
    
    
      <category term="前端技能" scheme="https://godbasin.github.io/tags/%E5%89%8D%E7%AB%AF%E6%8A%80%E8%83%BD/"/>
    
  </entry>
  
  <entry>
    <title>复杂渲染引擎架构与设计--2.插件的实现</title>
    <link href="https://godbasin.github.io/2023/06/15/render-engine-plugin-design/"/>
    <id>https://godbasin.github.io/2023/06/15/render-engine-plugin-design/</id>
    <published>2023-06-15T06:23:21.000Z</published>
    <updated>2023-06-15T06:23:07.422Z</updated>
    
    <content type="html"><![CDATA[<p>上一篇<a href="https://godbasin.github.io/2023/05/13/render-engine-render-and-collect/">《收集与渲染》</a>文章中，我们提到收集与渲染的拆分设计，对后续的业务拓展会更方便。</p><p>本文将介绍渲染插件的设计，渲染插件可用于各种新特性的拓展绘制。</p><a id="more"></a><h2 id="支撑业务的快速发展"><a href="#支撑业务的快速发展" class="headerlink" title="支撑业务的快速发展"></a>支撑业务的快速发展</h2><p>我们设计了渲染引擎，只能满足基础图形的收集和绘制，包括文本、线段、矩形、图像等。</p><p>而应用到具体的业务中，则是使用这些基础图形来绘制出业务相关的内容。因此，我们可以考虑将基础的能力进行封装，提供更便利的能力给到业务侧使用。</p><h3 id="分层封装底层能力"><a href="#分层封装底层能力" class="headerlink" title="分层封装底层能力"></a>分层封装底层能力</h3><p>举个例子，依然是表格的场景，由于大多数内容都是以单元格为基本单位来进行绘制的，我们则可以封装出一个提供按单元格绘制的中间层能力。</p><p>而当业务侧进行编辑操作时，更新的范围除了单个格子，也会包括整行、整列、整表、所选区域等情况，因此我们可以封装给到业务侧这些能力。</p><p>到这里，渲染引擎架构如图：</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-engine-plugin-design-1.jpg" alt></p><p>这种分层能力不仅在渲染引擎中可以用到，即使在我们平时的页面开发中，也完全可以用到。最常见的包括将页面布局做分层拆分，然后进行渲染。</p><p>不过现在基本上渲染的流程和实现都交由前端框架来负责，而 DOM 的布局则都交给浏览器本身的排版引擎去处理，用 Canvas 来绘制布局的场景的确很少。</p><h3 id="业务侧插件的设计"><a href="#业务侧插件的设计" class="headerlink" title="业务侧插件的设计"></a>业务侧插件的设计</h3><p>除了给上层业务提供封装好的能力，业务侧可以指定单元格范围进行重新渲染以外，还需要考虑另外一种的业务拓展场景：业务需要在单元格内绘制自己的内容，比如单元格背景高亮、一些特殊的图形属性单元格、图片绘制等等。</p><p>所以我们还需要给业务提供控制单元格绘制内容的能力。</p><p>前面一篇文章我们提到，每个单元格的绘制会有堆叠顺序，比如先绘制背景色，再绘制文字、边框线等等。那么，如果我们要给业务侧提供绘制的能力，他们同样需要可控制的堆叠顺序，和绘制内容的控制。</p><p>既然我们将渲染过程分成了收集和渲染两部分，渲染器的能力可以说是通用的能力，因为不管是单元格本身的绘制，还是业务侧新增的绘制内容，都离不开最基本的文字绘制、线段绘制、图形绘制、图像绘制等能力。</p><p>因此，我们可以考虑在收集过程中，通过提供插件的能力，让业务侧把想要绘制的内容收集起来。</p><p>插件提供的能力包括：</p><ol><li>定义收集时机，比如指定单元格绘制时进行收集，或是按行、按列、全表绘制时进行收集。</li><li>定义绘制配置，比如指定单元格绘制时，阻断正常单元格绘制内容。举个例子，考虑某个单元格不需要正常渲染文字内容，而是需要渲染特定的图形内容（如评星、进度条等），则可以通过配置控制。</li><li>添加绘制内容到收集器，绘制内容可指定绘制类型（文字、线段、图形等）以及堆叠顺序。</li></ol><p>插件的收集流程大概如图：</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-engine-plugin-design-2.jpg" alt></p><p>简单来说，插件的实现可能是：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 该代码只是写了个思路，不作为最终的实现方式</span></span><br><span class="line"><span class="keyword">abstract</span> <span class="keyword">class</span> BaseCollectPlugin &#123;</span><br><span class="line">    <span class="keyword">protected</span> collectWhen: <span class="string">'column'</span> | <span class="string">'row'</span> | <span class="string">'cell'</span> | <span class="string">'sheet'</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> collectConfig: &#123;</span><br><span class="line">        collectText: <span class="function"><span class="params">()</span> =&gt;</span> <span class="built_in">boolean</span>;</span><br><span class="line">        collectBorder: <span class="function"><span class="params">()</span> =&gt;</span> <span class="built_in">boolean</span>;</span><br><span class="line">        collectBackground: <span class="function"><span class="params">()</span> =&gt;</span> <span class="built_in">boolean</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> collect: <span class="function"><span class="params">()</span> =&gt;</span> ICollectInfo;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">constructor</span>(<span class="params">dependency</span>) &#123;</span><br><span class="line">        <span class="keyword">this</span>.collectWhen = dependency.collectWhen;</span><br><span class="line">        <span class="keyword">this</span>.collectConfig = dependency.collectConfig;</span><br><span class="line">        <span class="keyword">this</span>.collect = dependency.collect;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过这样的方式，业务可以自由地控制某些范围内单元格的绘制内容，且不需要侵入性地修改核心的绘制流程，拓展性得到了很好的提升。</p><p>现在越来越多的软件都支持通过插件的方式来拓展能力，也允许开发者一起来打造插件体系。对于插件的设计来说，独立性、安全性、拓展性都是比较重要的考虑方向。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>本文结合收集和渲染的渲染架构，设计了一套方便业务拓展的底层能力，包括提供支持可选范围的重新渲染能力，以及控制单元格绘制内容的插件能力。</p><p>很多时候我们都关注核心的架构能力，而往往忽略了业务的快速发展和迭代。实际上，架构就是为了不断变化的业务服务的，因此架构设计的时候，保留符合业务发展需要的拓展能力也是十分必要的。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;上一篇&lt;a href=&quot;https://godbasin.github.io/2023/05/13/render-engine-render-and-collect/&quot;&gt;《收集与渲染》&lt;/a&gt;文章中，我们提到收集与渲染的拆分设计，对后续的业务拓展会更方便。&lt;/p&gt;
&lt;p&gt;本文将介绍渲染插件的设计，渲染插件可用于各种新特性的拓展绘制。&lt;/p&gt;
    
    </summary>
    
      <category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
    
    
      <category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>复杂渲染引擎架构与设计--1.收集与渲染</title>
    <link href="https://godbasin.github.io/2023/05/13/render-engine-render-and-collect/"/>
    <id>https://godbasin.github.io/2023/05/13/render-engine-render-and-collect/</id>
    <published>2023-05-13T01:52:22.000Z</published>
    <updated>2023-05-13T01:52:52.299Z</updated>
    
    <content type="html"><![CDATA[<p>对于一般的渲染引擎来说，我们可以简单地拿到待渲染的数据，然后直接通过 Canvas/DOM/SVG 来将需要渲染的图形和内容渲染出来。</p><p>而在复杂场景下，比如需要自行排版的文本、表格和图形类，光是将要渲染的数据计算出来，便容易面临性能瓶颈，而对于样式多样、结构复杂的内容来说，绘制过程也同样会出现性能瓶颈。</p><a id="more"></a><p>本文我们主要针对 Canvas 绘制的场景，来考虑将绘制的流程分为收集和渲染两部分。</p><h2 id="渲染数据的收集"><a href="#渲染数据的收集" class="headerlink" title="渲染数据的收集"></a>渲染数据的收集</h2><p>很多时候，我们在后台数据库存储的只有图形的基本信息，对于需要排版计算的数据来说，则需要在拿到数据之后，再根据页面进行排版计算，完成后才能渲染到页面。</p><p>或许这样说会有些抽象，我们以表格的渲染为例来说明。</p><p>对于表格这样的产品来说，存储的往往是以单元格为基本单位的数据，如每个单元格的内容（可能是复杂的富文本、图片、图标结合）、样式（边框、背景色）、行列的宽高等。而在实际上页面渲染的时候，我们可能会根据行列宽高、每个单元格的边框线设置来绘制格子的布局。</p><p>除此之外，我们还可能需要考虑单元格内容是否会超出单元格，来判断是否需要截断渲染、是否需要换行显示等。这便要求我们需要对内容宽高进行测量，比如使用<code>CanvasRenderingContext2D.measureText()</code>测量文本宽度，这依赖了浏览器环境下的 API 能力。这意味着我们无法在后台提前计算好这些数据，因此无法通过提前计算来加速渲染过程。</p><p>于是，我们需要在前端拿到后台数据后，再进行相应的排版计算。</p><p>如果说一边计算一边绘制，则整个过程的耗时会比较长，用户也可能会看到绘制过程，该体验不是很友好。因此，我们可以在计算过程中，先把要最终绘制的数据结果先收集到一起，Canvas 绘制的时候则可以直接用。</p><h2 id="收集与绘制的功能划分"><a href="#收集与绘制的功能划分" class="headerlink" title="收集与绘制的功能划分"></a>收集与绘制的功能划分</h2><p>我们可以根据绘制内容，划分为以下的收集器和渲染器：</p><ul><li>线段数据收集和绘制（如表头、边框线等）</li><li>矩形数据收集和绘制（如背景色）</li><li>图像数据收集和绘制（如图片）</li><li>文本数据收集和绘制（如文字内容）</li><li>其他数据收集和绘制</li></ul><p>可见，收集器和渲染器的类型是一一对应的，渲染器在渲染的时候，可以直接从对应的收集器类型中获取数据，然后绘制。</p><p>还需要考虑一种情况，即相同的收集器和不同的收集器类型里，绘制内容有重叠时，需要考虑绘制堆叠的顺序。举个例子，单元格的文字需要在背景色上面，也就是说单元格的绘制需要比背景色要晚。</p><p>这意味着我们在收集的时候，还需要给收集的数据添加堆叠顺序，在绘制的时候，则按照堆叠顺序先后绘制。我们可以将收集器分为多个，每个收集器定义堆叠顺序，相同堆叠顺序的数据收集到一起。</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-engine-render-and-collect-1.jpg" alt></p><p>其实，我们使用一个收集器，通过给数据添加渲染类型，来将不同类型的数据放在一起，方便统一管理。在渲染的时候，则先根据绘制类型和堆叠顺序进行排序，再进行绘制。</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-engine-render-and-collect-2.jpg" alt></p><h2 id="渲染数据享元"><a href="#渲染数据享元" class="headerlink" title="渲染数据享元"></a>渲染数据享元</h2><p>前面我们在<a href>《前端性能优化–Canvas 篇》</a>一文中描述过，Canvas 上下文切换的成本消耗比较大，如果在复杂内容绘制的情况下，可能会导致性能问题。</p><p>使用收集器的一个好处是，我们可以将同类型同样式的渲染数据进行享元。对于样式完全一样的数据，收集器可通过对样式进行 hash 享元存储在一起。</p><p>这样绘制的时候，就可以将样式一样的内容一起绘制，减少 Canvas 上下文的切换。</p><p>举个简单的例子，假设线段的绘制支持颜色、粗细两种不同的样式，那么我们收集的时候可以将同颜色、同粗细的线段位置信息存储在一起：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> LineCollector &#123;</span><br><span class="line">  <span class="comment">// 以线段样式为 key，存储线段的开始和结束位置，如 [[startX, startY], [endX, endY]]</span></span><br><span class="line">  <span class="keyword">private</span> renderInfos: &#123; [key: <span class="built_in">string</span>]: <span class="built_in">number</span>[][] &#125; = &#123;&#125;;</span><br><span class="line"></span><br><span class="line">  addRenderInfo(renderInfo) &#123;</span><br><span class="line">    <span class="keyword">const</span> hash = <span class="keyword">this</span>.generateKey(renderInfo);</span><br><span class="line">    <span class="keyword">if</span> (!<span class="keyword">this</span>.renderInfos[hash]) &#123;</span><br><span class="line">      <span class="keyword">this</span>.renderInfos[hash] = [];</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">this</span>.renderInfos[hash].push([...renderInfo.position]);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">private</span> generateKey(lineProp: &#123; color: <span class="built_in">string</span>; thickness: numer &#125;): <span class="built_in">string</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">`<span class="subst">$&#123;lineProp.thickness&#125;</span>-<span class="subst">$&#123;lineProp.color&#125;</span>`</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过这样的方式，我们将要绘制的数据收集起来，方便 Canvas 进行更高效的渲染。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>通过将 Canvas 渲染过程拆分为收集和渲染两部分，架构上更清晰的同时，在一定程度上提升了渲染的性能和效率。而这样的架构设计，也更方便后续做更多的拓展，我会在后续篇章继续介绍。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;对于一般的渲染引擎来说，我们可以简单地拿到待渲染的数据，然后直接通过 Canvas/DOM/SVG 来将需要渲染的图形和内容渲染出来。&lt;/p&gt;
&lt;p&gt;而在复杂场景下，比如需要自行排版的文本、表格和图形类，光是将要渲染的数据计算出来，便容易面临性能瓶颈，而对于样式多样、结构复杂的内容来说，绘制过程也同样会出现性能瓶颈。&lt;/p&gt;
    
    </summary>
    
      <category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
    
    
      <category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>如何进行前端职业规划</title>
    <link href="https://godbasin.github.io/2023/04/06/front-end-career-planning/"/>
    <id>https://godbasin.github.io/2023/04/06/front-end-career-planning/</id>
    <published>2023-04-06T12:02:28.000Z</published>
    <updated>2023-04-06T12:02:40.065Z</updated>
    
    <content type="html"><![CDATA[<p>我们还没离开校园的时候，就已经知道要对自己未来的职业发展进行规划。但并没有人会来教会我们这些，而我们在一股脑扎进工作里以后，又常常因为忙也好因为懒也好，觉得目前状态还可以，就把职业规划也扔脑后了。</p><p>当我们想起来要考虑下自己未来工作方向的时候，常常是因为遇到了瓶颈。大多数人都是在遇到工作困境的时候，才会开始思考要怎么度过难关。但其实做工作规划最好的时候，就是在问题出现以前。</p><h2 id="找准自身定位"><a href="#找准自身定位" class="headerlink" title="找准自身定位"></a>找准自身定位</h2><p>想要对自己的未来制定一些方案，得知道自己要去哪里。在团队里，可以根据自身喜好和团队的方向来找到自己在团队中的位置。但职业规划和团队中的定位不一样，首先我们要确定自己未来的发展方向。</p><h3 id="未来发展方向"><a href="#未来发展方向" class="headerlink" title="未来发展方向"></a>未来发展方向</h3><p>对于程序员来说，未来的发展方向无非就几个：深挖技术领域、转型技术管理、转型其他类型管理、转行、考公务员等等。在前端领域，可以分为纯前端、全栈等方向，而纯前端和全栈也都各自有更加细分的领域，比如性能、渲染、动画绘制等等。</p><p>至于具体要选哪个方向，大都由个人偏好决定。一个人想要做什么事情，会同时受到很多因素的影响，除了自己对技术的热情和能力，还可能包括遇到的一些人和事，例如特别崇拜的某个开发、尊重的某位前辈、遇到过一些不公正的事情、或深感触动的一些事情，都可能会成为我们想要立志做某件事的契机。</p><p>很多时候，即使我们已经确认想要去往哪个方向，但实际上依然会被未来的某些事物改变。正如很多公司要求员工写 KPI，员工将 KPI 写得再详细，依然在半年后考核时需要重新修改，因为计划永远赶不上变化。但我们不能因为未来可能会变，就认为没有意义，也不去写 KPI。</p><p>这就好比我们在做习题本，本子的最后写好了答案，既然都知道自己最后都会看答案，那么我们做不做题、是否做错了都无关紧要了吗？显然不是的，做题是个需要思考计算的过程，通过最终的答案我们可以知道自己的思考方式是否有误、是否需要调整。写 KPI 也是一个道理，我们如果最初定的 KPI 与最终的不一样，是否需要去反思下为什么呢？KPI 本身的设计，不就是为了让我们确认自己规划做的事情，最终是否实现了吗？</p><p>因此，即使未来某个时候我们的职业方向会进行调整，此时此刻我们一样需要对此进行规划。通过规划我们才有了目标，有了目标，我们才会制定计划并为之努力实现。</p><h3 id="扬长避短很有效"><a href="#扬长避短很有效" class="headerlink" title="扬长避短很有效"></a>扬长避短很有效</h3><p>其实我们每个人也都有各自擅长的部分，一般来说工作中更有效的方式是扬长避短，而不是花很多时间去补齐短板。我们常常会听到其他人说，“你没做过这些、要去挑战自己”，挑战自己的确是需要的，多尝试去做一些事情也总是好的。</p><p>如果我们需要在这场竞争中生存下来，必须拥有自己的不可替代性，也称之为技术壁垒。如果我们有擅长的领域，不妨尝试去深挖这样的领域，这样是一个相对容易的选择。</p><p>很多时候，也正是因为热爱和喜欢这些领域，我们才愿意投入更多的精力，因此在某种程度上也会更擅长。而做自己喜欢的事情的时候，即使再忙再苦再累，也一样会觉得很开心和值得。相反，如果做着不喜欢的工作时，每时每刻都是一种煎熬，每天上班就像去上坟。</p><p>可以的话，找一个可以发挥你自身优势的地方，那样的你会闪闪发光，你也会因此爱上你自己。</p><h3 id="你的出处并不能代表什么"><a href="#你的出处并不能代表什么" class="headerlink" title="你的出处并不能代表什么"></a>你的出处并不能代表什么</h3><p>我刚进入互联网的时候，非科班出身、缺乏开发经验，自学了一周多就疯狂投简历。找工作很头疼，即使是裸辞的一番热血，再热烈也很容易被浇灭。最后虽然顺利找到了工作，但工资少得可怜。</p><p>那会从华为出来，待遇的落差总会不断地提醒我，到底是为了什么呢？但满怀的热情使得我每天上班充满干劲，下班后也继续在床上打着灯看书和写代码学习。那是 jQuery 横行的年代，似乎只需要掌握了它，你就能所向披靡。还有 CSS/CSS3 动画等，对 CSS 的掌握基本上是 10%的理解加上 90%的实践日积月累不断沉淀的。</p><p>那段时间可以感觉到成长很快，几位后台开发小哥哥带着入门，告诉我需要学习哪些知识、可以去哪些网站上学，然后就停不下来了。我曾经在面试的时候说自己学习能力很强，但是通常别人会问，你怎么证明呢？</p><p>不知道为什么，现在似乎大家多多少少都会不正视外包，“要不是能力不够也不会当外包”、“不能指望他们能学会什么”这样的话也经常会听到。可能是因为很多人的经历和体验里，都是比较顺畅阻力较少。而我也很荣幸曾经置于职业的低谷，使得很多时候能看到更多的可能性。</p><p>同样的，如今很多公司在招人的时候对学历的要求也越来越高，这是由于竞争市场资源过剩导致的，公司或许在性价比等方面考虑进行这样的调整，但我们不能自己限制住自己。我也见过一些特别厉害的开发，他们并不一定来自很好的学校。只是因为有着一股认真钻研的劲，他们看不到所谓的比较、竞争，专心致志地沉浸在自己的世界中，并做出了很多的成绩。</p><p>是否科班出身、是否外包出身、是否学历优秀，这些或许对其他人来说会有影响，但这些都不应该影响我们自身要去努力，不应该成为我们去给自身贴上一些标签的理由。</p><p>而如今大家都说什么寒冬到来、前端已死，要问我的想法，大概是世上无难事、只怕有心人罢了。</p><p>真正的热爱，从来不会因一份怎样的工作而受到影响，只不过如今大多数人更在意利益得失而已。</p><h3 id="将目标放长放远"><a href="#将目标放长放远" class="headerlink" title="将目标放长放远"></a>将目标放长放远</h3><p>或许你会觉得制定计划不靠谱，因为事情永远都在变化。工作总是很复杂，不可控制的因素太多了。例如小明想要深入钻研浏览器渲染的方向，但实际上团队负责的业务都比较简单，因此小明常常需要快速上线某个新模块，节奏一直慢不下来，也完全没有机会和精力去研究自己的东西。</p><p>遇到这种情况，可以尝试将眼光再放长远一些。如果将职业规划比作一场马拉松，首先我们得确认目标是什么，是拿到前面的名次还是完成这场比赛？以前参加过中长跑的田径比赛，教练在比赛前会叮嘱我们几件事：</p><ol><li>起跑的时候不要用力过猛，但需要让自己保持在一定的名次以内。  </li><li>过程中可以选择排在前面节奏比较好的选手，紧跟在对方后面。领跑比跟跑更累，紧紧贴在对方身后可以让对方心理上有压力。  </li><li>除了最后的冲刺，最好要保持匀速的节奏，因为变速跑会消耗更多的体力。  </li></ol><p>其实做职业规划和长跑很相似，我们需要确定一个较远的目标，然后控制好节奏、切忌着急和用力过猛。以我自己为例，一开始我想要往前端深度的领域发展，但我的起跑线是非科班+外包。显然，我无法一下子到达自己想去的地方，而此时自身的实力也无法和想要的岗位相匹配，因此我整体的职业路线是：外包公司前端 -&gt; 中规模公司前端 -&gt; 大公司重后台的业务部门前端 -&gt; 大公司重前端业务部门前端。</p><p>这是一个经历了好些年的过程，每当我觉得在所在的团队中没法获得更多成长的时候，就会选择进入下一个阶段。如果目的很明确的话，你会清晰地知道自己什么时候该调整、接下来要去哪。</p><p>中间也遇到过一些团队，虽然团队中缺乏我想要的复杂大型前端业务项目，但团队会给道其他的机会，例如待遇上的回报、往管理方向发展带团队等等。很多条件都比较诱人，团队也的确给到了足够的诚意希望我留下来。但我知道自己想要的是前端领域的深挖，如果现在因为有其它诱惑而暂时选择放弃，以后遇到同样的困境是不是每次都会选择放弃呢？</p><p>因此，基本上我都选择了按照原目标继续往前走。有管理者觉得很疑惑，大家都往钱多的地方走，他问我是不是对钱没什么需要。我当时的回答是，把该学的知识和技能都掌握，如何赚钱它不该是我需要担心的问题。</p><p>将眼光放远一些，直到我们建立起自己的技术壁垒，在那之前即使挣到更多的钱、可以带团队干活，也依然可能会面临被这个行业抛弃。当然，这里的前提是我自身想要往技术的方向去发展。如果想要往管理方向去发展，一些带团队的经验也是很有帮助的。</p><p>如果现在的你距离自己的目标太远，那么不妨尝试找到去向最终目标的一个小目标，先往小目标去努力。通往目标的路上干扰很多，但如果你足够地坚定，总有一天也能去到自己想去的地方的。</p><h2 id="职业发展中面临的选择"><a href="#职业发展中面临的选择" class="headerlink" title="职业发展中面临的选择"></a>职业发展中面临的选择</h2><p>对于每个技术人来说，都会遇到一些发展方向的选择困惑，比如“该往深度发展”还是“该往广度发展”。前端也不例外，我们的发展方向种类越来越多，但一个人的精力总是有限的，我们还是需要做出选择。</p><h3 id="全栈-or-纯前端？"><a href="#全栈-or-纯前端？" class="headerlink" title="全栈 or 纯前端？"></a>全栈 or 纯前端？</h3><p>如今随着 Node.js 的普及，也有不少的前端开发慢慢转型做全栈、大前端等方向。</p><p>的确，对于有全栈工作经验的人来说，找工作的时候会更吃香。但我们日常工作中是否都有机会去接触后台开发、客户端开发这些内容呢？是否一定需要有这样的工作经验才能获得更好的发展呢？</p><p>很多时候，前端由于门槛较低，很多的前端开发（比如我）都不是计算机专业出身。我们对于计算机基础、网络基础、算法和数据结构等内容掌握很少，更多时候是这些知识的缺乏阻碍了我们在程序员这一职业的发展，这也是为什么很多前端开发苦恼自己到达天花板，想着转型全栈或者后台就能走得更远。</p><p>这其实是个误区。后台开发由于开发语言、服务器管理、计算机资源等工作内容的不一致，对于专业基础的要求更高，因此看上去似乎比前端能走得更远。但随着成熟的解决方案的出现，像分布式部署和管理、全链路跟踪等，以及运维和 DBA 等职位的出现、后台基本框架的完善，更多的后台开发技术选型的范围不大，在开发过程中也同样会偏向业务开发，因此更多的关注点会落在业务风险梳理、问题定位和追踪、业务稳定性、效率提升等地方。对于全栈开发中的后台开发，可能涉及的内容会更加局限一些。</p><p>所以，其实我们在日常工作中也可以更多地关注后台的实现和能力，除了可以更好地配合和理解后台的工作外，还可以提升自己对后台工作内容的理解。当然，最重要的依然是扎实地补充计算机基础知识。</p><p>全栈开发经验可能让我们更容易地找到工作，但只有基础知识的掌握足够深入，才可以在接触后台开发、终端开发等内容的时候，有足够的能力去快速高效地解决问题。</p><p>相比转型全栈，其实纯前端可深挖的方向也很多，包括关注性能的各种深入的性能优化领域、关注效能的脚手架/CI/CD 等构建领域、关注可维护的项目与代码设计等架构领域、关注浏览器渲染的游戏引擎/WebGL 等特殊领域。选择走广度方向会要求有足够丰富的项目经验，而选择走深度方向只需要在某个领域有足够深刻的理解和突破，就可以建立起稳固的技术壁垒。</p><h3 id="ToB-or-ToC"><a href="#ToB-or-ToC" class="headerlink" title="ToB or ToC"></a>ToB or ToC</h3><p>对于前端同学来说，我们也常常会纠结与 ToB 和 ToC 的工作内容选择，它们之间区别多在于用户群体和数量。</p><p>一般来说，ToB 的业务服务于某一类用户群体，因此会根据服务对象的不一样，工作重点有所区别。例如，如果服务于银行，会对技术方案/安全性要求严格；如果服务于政府机构，则可能需要兼容较低版本的 IE 浏览器（笑），技术选型比较局限。但通常来说，ToB 业务的用户量并不会特别大，对性能要求较低，有些情况下也会由于机器部署环境封闭的原因，对网络和安全性要求较低，因此 ToB 业务可以更多关注开发效率提升、技术管理选型、项目可维护性等方面。</p><p>ToC 的业务用户量较大，对加载性能、浏览器兼容性等都要求很高，因此常常需要进行性能优化、兼容性检测、实时监控、SEO 优化等工作。</p><p>找工作的时候，拥有 ToC 业务开发经验通常会比拥有 ToB 业务开发经验的优势要大一点，因为 ToC 对前端的各个角度要求都相对较高。但在真正的工作中，由于精力和工作内容分配的问题，很多参与 ToC 业务的人更多只会关注到自己负责的模块部分，因此很多时候并没有掌握到较完整的 ToC 业务相关的关键技术方案。同样，即使是在做的是 ToB 业务，也有不少小伙伴会有很多时间去研究一些新技术、做很多的选型调研，也可以在这个过程中获得很好的成长。</p><p>所以，决定我们能否掌握更多的、成长更快的，或许业务的影响比我们想象中还要小，最终还是取决于自己。</p><h3 id="赚钱-or-个人成长？"><a href="#赚钱-or-个人成长？" class="headerlink" title="赚钱 or 个人成长？"></a>赚钱 or 个人成长？</h3><p>当一份很赚钱但没什么技术含量的工作（下面成为工作 A），以及一份有趣又具备足够挑战性的工作（工作 B），这样两份工作放在我们面前的时候，大概很多人都会犹豫。这的确是一个很现实的问题，钱可以让我们买到很多自己想要的东西，但它却没法买来成长极快的工作经历。丰富的工作经历可以给我们带来竞争力，但短期内可能会带来经济上的困扰。</p><h4 id="分析每个阶段的需求"><a href="#分析每个阶段的需求" class="headerlink" title="分析每个阶段的需求"></a>分析每个阶段的需求</h4><p>前面我有表达过自己的观点，如果我们掌握了足够的技术和能力，就不会担心自己赚不到钱。如果以这个角度来看，工作 B 显然会是我们的选择。但这个世界并不都是非黑即白的，如果我们身边大多数的小伙伴都选择了向钱看，他们每年赚到的钱甚至是我们的一两倍，大多数人都无法不为所动。当然，如果有一份工作又能成长又能赚钱的工作最好，但遇到这样的选择概率会很低。</p><p>我们可以将自己的职业发展分为几个阶段，然后针对每个阶段分析该阶段中最重要的一些目标，这些目标可以是自身能力的提升，也可以是工资的上涨，还可以是职级的晋升等等。那么，当我们遇到困扰的选择时，可以选择当前阶段中比较重要的目标相关的工作内容。</p><p>举个例子，张三家境不好，他希望毕业工作之后可以尽快帮家里还清贷款，那么张三可以先选择赚钱更多的工作 A。当贷款还清以后，张三可以进入下一个阶段，如果这个阶段他觉得自己要提升实力来维持在工作中的竞争力，那么他就可以选择成长更快的工作 B。</p><p>和张三不同，李四只想攒点钱买个房子，对他来说工作就只是一份工作。但如果他不提升自己的能力，或许就会面临被淘汰、也没法赚到足够的钱，因此李四可能需要在某个阶段选择更具挑战性的工作 B。</p><p>每个人的愿望都不一样，有些人希望攒一些积蓄、买个小房子、过点小日子，也有人希望在职场叱咤风云、留下自己的名字，还有人希望沉浸在自己的世界、一直钻研某个领域。我们的愿望决定了我们最终的目标，而为了实现这个目标，中间也可能会做些看起来与目标相背离的事情。</p><p>我们在做的事情到底有没有用、能不能到达想去的远方，这些只有时间能证明。很多时候，每个人看到的只有当下的片刻，我们不能因为现在眼里的自己不像自己，就感到不甘、难过、想要自暴自弃。同样也不能因为目前眼里其他人的趋利避害、趋于世俗而瞧不起对方。或许有些人我们无法理解也无法接受，但即使认为不是一路人，也该给予他们足够的尊重。</p><h4 id="为什么不可以一边赚钱一边做喜欢的事情呢"><a href="#为什么不可以一边赚钱一边做喜欢的事情呢" class="headerlink" title="为什么不可以一边赚钱一边做喜欢的事情呢"></a>为什么不可以一边赚钱一边做喜欢的事情呢</h4><p>大多数人都存在这样的误区，认为做自己喜欢的事情就很幸运了，不应该再期望能赚到多少钱。</p><p>我也看到有些人为了追求自己想做的工作，愿意“不要工资”、“只要给我做的机会就好了”。做自己喜欢的事情固然很好，但这并不意味着我们必须要付出很多很多的代价。从公司的角度看，会因为一个人零成本而录用他的几率不大，更多的时候还是愿意付出更多的成本招聘一个有足够能力的人。如果想要争取一份工作，我们要做的是努力提升自己、让自己配得上这份工作的职责要求。</p><p>当然，也有人是因为想要转行，认为自己没有相关的工作经历，被录用的概率太低，认为如果自己不要钱对方可以给自己机会去学习就足够了。对于这种想法，我依然保持上述的建议：尽可能先提升自己的能力，通过自学也好，出去从有所关联的小职位、小公司开始做起也都是可以的。不需要提出不要工资这样的条件，这样只会更让对方认为你能力欠缺。</p><p>有些时候，我们还会陷入另外一个误区：如果因为做自己喜欢做的事情而获利，这种喜欢就不纯粹了。但谁能告诉我，为什么我们不能一边做自己喜欢的事情一边赚钱呢？如果我们做自己喜欢的事情，同时还能赚到钱，那不是会让自己更有动力、有更多的成本去继续做这些喜欢的事情吗？</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;我们还没离开校园的时候，就已经知道要对自己未来的职业发展进行规划。但并没有人会来教会我们这些，而我们在一股脑扎进工作里以后，又常常因为忙也好因为懒也好，觉得目前状态还可以，就把职业规划也扔脑后了。&lt;/p&gt;
&lt;p&gt;当我们想起来要考虑下自己未来工作方向的时候，常常是因为遇到了瓶
      
    
    </summary>
    
      <category term="前端技能提升" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E6%8A%80%E8%83%BD%E6%8F%90%E5%8D%87/"/>
    
    
      <category term="前端技能" scheme="https://godbasin.github.io/tags/%E5%89%8D%E7%AB%AF%E6%8A%80%E8%83%BD/"/>
    
  </entry>
  
  <entry>
    <title>为什么项目复盘很重要</title>
    <link href="https://godbasin.github.io/2023/03/21/why-project-reviews-are-important/"/>
    <id>https://godbasin.github.io/2023/03/21/why-project-reviews-are-important/</id>
    <published>2023-03-21T03:59:35.000Z</published>
    <updated>2023-03-21T04:00:09.730Z</updated>
    
    <content type="html"><![CDATA[<p>当开发的个人能力成长到一定程度时，日常工作不再是缝缝补补、修修 bug、打打下手。</p><p>开发时间足够长时，我们常常会以项目的形式参与到具体的开发中，可能会负责项目的主导，或是作为核心开发负责某个模块、某个技术方案的落地。</p><p>在项目进行的每个阶段，我们都可以通过同样的方式去提升自己：</p><ol><li>事前做预期。</li><li>事后做复盘。</li></ol><a id="more"></a><h2 id="事前做预期"><a href="#事前做预期" class="headerlink" title="事前做预期"></a>事前做预期</h2><p>就像在代码开发前进行架构设计一样重要，我们在项目开始前，需要对项目的整个过程进行初步的预期，包括：</p><ol><li>预期功能是否能实现？存在哪些不确定的功能？</li><li>预计的工作量和分工排期是怎样的？</li><li>每个阶段（开发、联调、产品体验、提测、发布）的时间点大概是怎样的？</li><li>哪些工作涉及外部资源的依赖和对接（交互/设计/接口协议等），是否存在延期风险？</li><li>如果存在风险，有没有什么方式可以避免？</li></ol><p>这么做有什么好处呢？如果不做方案调研和项目预期管理，那么对于项目过程中的风险则很难预测。这会导致项目的延期，甚至做到一般发现做不下去了。</p><p>在我们日常的工作中，这样的情况常常会遇到，很多人甚至对需求延期都已经习以为常了，认为需求能准时上线才是稀奇的事情。正因为大家都是这样的想法，我们更应该把这些事情做好来，这样才可以弯道超车。</p><p>首先，在项目开始的时候，需要进行工作量评估和分工排期。</p><h3 id="如何进行合理的分工排期"><a href="#如何进行合理的分工排期" class="headerlink" title="如何进行合理的分工排期"></a>如何进行合理的分工排期</h3><p>进行工作量评估的过程可以分为三步：</p><ol><li>确认技术方案，以及分工合作方式。</li><li>拆分具体功能模块，分别进行工作量评估，输出具体的排期时间表。</li><li>标注资源依赖情况和协作存在的风险，进行延期风险评估。</li></ol><p>当我们确认好技术方案之后，可以针对实现细节拆分具体的功能模块，分别进行工作量的预估和分工排期。具体的分工排期在多人协作的时候是必不可少的，否则可能面临分工不明确、接口协议未对齐就匆忙开工、最终因为各种问题而返工这些问题。</p><p>进行工作量评估的时候，可以精确到半天的工作量预期。对独自开发的项目来说，同样可以通过拆解功能模块这个过程，来思考具体的实现方式，也能提前发现一些可能存在的问题，并相应地进行规避。</p><p>提供完整的工作量评估和排期计划表（精确到具体的日期），可以帮助我们有计划地推进项目。在开发过程中，我们可以及时更新计划的执行情况，团队的其他人也可以了解我们的工作情况。</p><p>工作量评估和排期计划表的另外一个重要作用，是通过时间线去严格约束我们的工作效率、及时发现问题，并在项目结束后可针对时间维度进行项目复盘。</p><p>为了确保项目能按照预期进行，我们还要对可能存在的风险进行分析，提前做好对应的准备措施。</p><h3 id="对项目风险进行把控"><a href="#对项目风险进行把控" class="headerlink" title="对项目风险进行把控"></a>对项目风险进行把控</h3><p>我们在项目开发过程中，经常会遇到这样的情况：</p><ul><li>因为方案设计考虑不周，部分工作需要返工，导致项目延期</li><li>在项目进行过程中，常常会遇到依赖资源无法及时给到、依赖方因为种种原因无法按时支援等问题，导致项目无法按计划进行</li><li>团队协作方式未对齐，开发过程中出现矛盾，反复的争执和调整协作方式导致项目延期</li></ul><p>一个项目能按照预期计划进行，技术方案设计、分工和协作方式、依赖资源是否确定等，任何一各环节出现问题都可能导致整体的计划出现延误，这是我们不想出现的结果。</p><p>因此，我们需要主动把控各个环节的情况，及时推动和解决出现的一些多方协作的问题。</p><h2 id="事后做复盘"><a href="#事后做复盘" class="headerlink" title="事后做复盘"></a>事后做复盘</h2><p>很多开发习惯了当代码开发完成、发布上线之后就结束了这个项目，其实他们遗漏了一个很重要的环节：复盘。</p><p>对于大多数开发来说，很多时候都不屑于主动邀功，觉得自己做了些什么老板肯定都看在眼里，写什么总结和复盘都是刷存在感的表现。实际上老板们每天的事情很多，根本没法关注到每一个人，我以前也曾经跟老板们问过这样一个问题：做和说到底哪个重要？</p><p>答案是两个都重要。把一件事做好是必须的，但将这件事分享出来，可以同样给团队带来更多的成长。</p><h3 id="用数据说话"><a href="#用数据说话" class="headerlink" title="用数据说话"></a>用数据说话</h3><p>性能优化的工作可以用具体的耗时和 CPU 资源占用这些数据来做总结，工具的开发可以用接入使用的用户数量来说明效果，这种普普通通的项目上线，又该怎么表达呢？</p><p>我们可以用两个维度复盘：</p><ol><li>时间维度。</li><li>质量维度。</li></ol><p>其中，时间维度可以包括：</p><ul><li>项目的预期启动、转体验、提测、灰度、全量时间</li><li>项目的最终启动、转体验、提测、灰度、全量时间</li></ul><p>通过预期和最终结果的对比，我们可以直观看到是否存在延期等情况，分析原因分别是什么（比如方案设计问题、人员变动、协作方延期等）</p><p>如下图，假设项目分为一期、二期，我们可以在一期结束后，进行复盘分析并改进。同时还可以以时间线的方式对比开发时间结果：</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/my-career5-6.jpg" alt></p><p>除了时间维度以外，我们还可以通过衡量项目质量的方式来复盘，比如：</p><ul><li>代码是否有单测、自动化测试保证质量</li><li>产品体验阶段的问题、提测后 BUG 分别有多少</li><li>灰度和全量后的用户反馈有多少</li></ul><p>我们需要分析各个阶段存在的质量问题，并寻找原因（比如技术方案变更时考虑不全、设计稿还原度较低、自测时间不足等）。质量的维度同样可以用对比的方式来展示：</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/my-career5-7.jpg" alt></p><p>所以，为什么项目复盘很重要呢？</p><ol><li>及时发现自己的问题并改进，避免掉进同一个坑。</li><li>让团队成员和管理者知道自己在做什么。</li><li>整理沉淀和分享项目经验，让整个团队都得到成长。</li></ol><h3 id="输出结果"><a href="#输出结果" class="headerlink" title="输出结果"></a>输出结果</h3><p>很多人会觉得做一个普通的前端项目，从开发到上线都没什么难度。一个字：“干”就完了。</p><p>实际上，项目的管理、推动和落地是工作中不可或缺的能力，这些不同于技术方案设计、代码编写，属于工作中的软技能。但正是这样的软技能会很大地影响我们的工作成果，也会影响自身的成长速度，是升职加薪的必备技能。</p><p>职场之所以让人不适，很多时候是由于它无法做到完美的公平。对于程序员来说，同样如此。</p><p>因此，为了能让自己付出的努力事半功倍，阶段性的输出是必不可少的。对于项目复盘来说，我们可以通过团队内外分享、邮件复盘总结等方式进行输出。</p><p>一般来说，可以通过几个方面来总结整理：</p><ol><li>项目背景，比如为什么启动项目、目标是什么之类。</li><li>技术方案，是否做了技术选型、架构设计等。</li><li>项目结果，时间维度和质量维度，最好有数据佐证。</li><li>未来规划/优化方向。</li></ol><p>通过对项目进行复盘，除了可以让团队其他人和老板知道我们做了些什么，更重要的是，我们可以及时发现自身的一些问题并改进。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>本文介绍了在项目开发过程中，要如何做好前期的准备，又该如何在项目结束后进行完整的复盘。</p><p>对于大部分前端开发来说，接触工具和框架开发、参与开源项目的机会比较少，很多时候我们写的都是“枯燥无聊”的业务代码。我们总认为只有做工具才会比较有意思、有技术挑战，很多时候会先入为主，认为业务代码写得再好也没用，也渐渐放弃了去思考要怎么把事情做好。</p><p>其实不只是工作中，我们生活里也可以常常进行反思和总结，这样我们的步伐才可以越跑越快。成长的过程中总会遇到各式各样的问题，有些问题被我们视而不见，有些问题我们选择了躲开，但其实我们还可以通过迎面应战、解决并反思的方式，在这样一次次战斗中快速地成长。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;当开发的个人能力成长到一定程度时，日常工作不再是缝缝补补、修修 bug、打打下手。&lt;/p&gt;
&lt;p&gt;开发时间足够长时，我们常常会以项目的形式参与到具体的开发中，可能会负责项目的主导，或是作为核心开发负责某个模块、某个技术方案的落地。&lt;/p&gt;
&lt;p&gt;在项目进行的每个阶段，我们都可以通过同样的方式去提升自己：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;事前做预期。&lt;/li&gt;
&lt;li&gt;事后做复盘。&lt;/li&gt;
&lt;/ol&gt;
    
    </summary>
    
      <category term="前端技能提升" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E6%8A%80%E8%83%BD%E6%8F%90%E5%8D%87/"/>
    
    
      <category term="前端技能" scheme="https://godbasin.github.io/tags/%E5%89%8D%E7%AB%AF%E6%8A%80%E8%83%BD/"/>
    
  </entry>
  
  <entry>
    <title>如何设计与管理一个前端项目</title>
    <link href="https://godbasin.github.io/2023/01/12/design-and-manage-front-end-project/"/>
    <id>https://godbasin.github.io/2023/01/12/design-and-manage-front-end-project/</id>
    <published>2023-01-12T02:38:02.000Z</published>
    <updated>2023-01-12T02:38:18.117Z</updated>
    
    <content type="html"><![CDATA[<p>如果说基础知识的掌握是起跑线，那么使大家之间拉开差距的更多是前端项目开发经验和技能。对于一个项目来说，从框架选型和搭建，到项目维护、工程化和自动化、多人协作等各个方面，都需要我们在参与项目中不断地思考和改进，积累经验。</p><a id="more"></a><p>本文将要介绍：</p><ul><li>前端项目设计</li><li>前端项目管理</li></ul><h2 id="前端项目设计"><a href="#前端项目设计" class="headerlink" title="前端项目设计"></a>前端项目设计</h2><p>除了具体的前端领域知识以外，当我们开始负责起整个前端项目的管理时，需要具备一些方案选型、架构设计、项目瓶颈识别并解决等能力。</p><h3 id="前端项目搭建"><a href="#前端项目搭建" class="headerlink" title="前端项目搭建"></a>前端项目搭建</h3><p>很多时候，我们的项目在刚搭建的时候规模会比较小，因此在项目启动阶段需要做简化，来保证项目能快速地上线。但从长期来看，一个项目还需要考虑到拓展性。换句话说，当项目开始变得较难维护的时候，我们就要进行一些架构或者流程上的调整。</p><p>在项目开始之前，我们需要做一系列的规划，像项目的定位（to B/C）、大小，像框架和工具的选型、项目和团队规范等，包括：</p><ul><li>前端框架选择：基于团队成员偏好和能力，选择适合的前端框架</li><li>工具库选择：基于项目规模，选择是否需要路由管理、状态管理等工具库</li><li>自动化工具：基于成员规模和项目状态（快速上线、稳定维护等），选择是否需要代码构建、自动化测试等自动化工具，以及搭建持续集成、持续部署等自动化流程</li><li>项目流程规范：使用一致的项目规范，包括项目代码结构、代码规范、开发流程规范、多人协作规范等内容</li></ul><p>项目的维护永远是程序员的大头，多是“前人种树，后人乘凉”。但是很多时候，大家会为了一时的方便，对代码规范比较随意，就导致了我们经常看到有人讨论“继承来的代码”。</p><p>代码规范其实是团队合作中最重要的地方，使用一致的代码规范，会大大减少协作的时候被戳到的痛点。好的写码习惯很重要，包括友好的变量命名、适当的注释等，都会对代码的可读性有很大的提升。但是习惯是每个人都不一样，所以在此之上，我们需要有这样统一的代码规范。</p><p>一些工具可以很好地协助我们，像 Eslint 这样的工具，加上代码的打包工具、CI/CD 等流程的协助，可以把一些规范强行标准化，达到代码的统一性。还有像 prettier 这样的工具，可以自动在打包的时候帮我们进行代码规范的优化。</p><p>除了这些简单的命名规范、全等、单引双引等代码相关的规范，还有流程规范也一样重要。比如对代码进行 code review，尤其在改动公共库或是公共组件的时候。</p><p>最重要的还是多沟通。沟通是一个团队里必不可少、又很容易出问题的地方，我们要学会沟通和表达自己。</p><h3 id="洞察项目瓶颈"><a href="#洞察项目瓶颈" class="headerlink" title="洞察项目瓶颈"></a>洞察项目瓶颈</h3><p>我们常常会觉得自己做的项目没有什么意思，每天都是重复的工作、繁琐的业务逻辑、糟糕的历史遗留代码，反观其他人都在做有技术、有难度、有挑战性的工作，越会难以喜欢上自己负责的工作。</p><p>实际上，那些会让我们觉得枯燥、反复、杂乱的工作内容，更是可以去改善做好、并能从中获得成长的地方。涉及前端工作的业务，只有极少一部分的业务例如涉及多人协同的在线文档、或是用户量很大的业务如电商、直播、游戏等，这些业务重心可能会稍微倾向前端，更多时候前端真的会处于编写页面、最多就用 Node.js 写写接入层等状况。好的业务可遇不可求，我们在遇到这些业务之前，就什么都不做了吗？</p><p>大多数工作中，对开发的要求都不仅限于实现功能。如果只是编写代码，刚毕业的应届生花几周时间也一样能做到，那么我们的优势在哪里呢？洞察工作中的瓶颈，并有足够的能力去设计方案、排期开发、解决并复盘，这些技能更能突显我们在岗位上的价值和能力。对团队来说，更需要这样能主动发现并解决问题的成员，而不是安排什么就只做什么的螺丝钉。</p><p>一般来说，用户量较大的项目的瓶颈通常会在兼容性、性能优化这些方面；对于一次性的活动页面，挑战点存在于如何高效地完成一次活动页面的开发或者配置，通常会使用配置系统、结合拖拽以及所见即所得等方式来生成页面；对于经常开发各式各样的管理端系统，优化方向则在于怎么通过脚手架快速地生成需要的项目代码、如何快速地发布上线等。我们要做的，就是找到工作中让自己觉得烦躁和不爽的地方，然后去改进优化它们。</p><h3 id="方案调研与选型对比"><a href="#方案调研与选型对比" class="headerlink" title="方案调研与选型对比"></a>方案调研与选型对比</h3><p>找到项目的痛点或是瓶颈后，就需要设计相应的方案去解决它们。而当我们需要投入人力和时间成本去做一件事，就需要面临一个问题：如何让团队认同这件事情、并愿意给到资源让我们去完成它？</p><p>可以通过前期的调研，找一些业界相对成熟的方案作为参考。如果有多套方案，则需要对这些方案进行分析比较。例如，小明最近需要针对项目进行自动化性能测试能力的支持，因为项目规模大、模块多、参与开发的成员也有几十人，经常因为一些不同模块的变更导致项目的性能下降却没法及时发现问题，往往是等到用户反馈或是某次开发、产品或者测试发现的时候才得知。</p><h2 id="前端项目管理"><a href="#前端项目管理" class="headerlink" title="前端项目管理"></a>前端项目管理</h2><p>不同于做工具和框架、参与开源协同，很多时候我们写的都是业务代码。我们总认为只有做工具才会比较有意思、也有技术挑战，但是业务代码就没有可以提升技术、挑战自己的地方了吗？其实并不是，很多时候我们先入为主、认为业务代码写得再好也没用、自己放弃了去做这样的事情。多多思考，你会发现每个项目都可以大有可为，你的未来也可以大不一样。</p><h3 id="合理的分工排期"><a href="#合理的分工排期" class="headerlink" title="合理的分工排期"></a>合理的分工排期</h3><p>很多开发在进行编码实现功能的时候，都直接想到哪写到哪，也常常会出现代码写到一半发现写不下去，结果导致重新调整实现，最终项目从预期的一周变成了一个月、迟迟上线不了的问题。</p><p>当我们确认好技术方案之后，可以针对实现细节拆分具体的功能模块，分别进行工作量的预估和分工排期。这一步骤在多人协作的时候是必不可少的，否则可能面临分工不明确、接口未对齐就匆忙开工、最终因为各种问题而返工这些问题。而对单人项目来说，也可以通过拆解功能模块这个过程来思考具体的实现方式，也能提前发现一些可能存在的问题，并相应地进行规避。</p><p>提供完整的工作量评估和时间表，我们可以比较有计划地进行开发，同时团队的其他人也可以了解我们的工作情况，有时候大家能给到一些建议，也能避免对方不了解我们到底在做什么而导致的一些误会。而排期预估的另外一个重要作用，则是通过时间线去严格约束我们的工作效率、及时发现问题，以及项目结束后可针对时间维度进行项目复盘。</p><h3 id="风险把控"><a href="#风险把控" class="headerlink" title="风险把控"></a>风险把控</h3><p>前面有说到，我们需要在参与项目的过程中具备 Owner 意识，即使这个项目并不是我们主导。风险把控则是作为 Owner 必须掌握的一个能力，我们需要确保项目能按照预期进行，则需要主动发现其中可能存在的风险并提前解决。</p><p>除了因为方案设计考虑不周而导致的一些返工风险，我们在项目进行过程中常常也会遇到依赖资源无法及时给到、依赖方因为种种原因无法按时支援、团队协作出现矛盾等各种问题，任何一块出现问题都可能导致整体的工期出现延误，这是我们不想出现的结果。因此，我们需要主动把控各个环节的情况，及时推动和解决出现的一些多方协作的问题。</p><p>通过前期准备的这些方案和工具，提前控制好一些可预见的风险，开发过程会更加顺利。但是如果我们的效果只有这些的话，很多时候是无法证明自己做了这么多事情的价值。那么，我们可以尝试用数据说话。</p><h3 id="及时反馈与复盘"><a href="#及时反馈与复盘" class="headerlink" title="及时反馈与复盘"></a>及时反馈与复盘</h3><p>很多开发习惯了当代码开发完成、发布上线之后就结束了这个项目，其实他们遗漏了一个很重要的环节：复盘。通过复盘这种方式，我们可以发现自身的一些问题并改进，还可以让团队其他人以及管理者知道我们做了些什么，这是很重要的。</p><p>复盘的总结内容，可以通过邮件的方式发送给团队以及合作方，同时还可以作为自身的经验沉淀，后续更多项目中可以进行参考。如果使用得当，我们还可以通过这种方式来影响我们的团队和管理者，也是向上管理的一种方法。</p><p>但其实不只是工作中，我们生活里也可以常常进行反思和总结，这样我们的步伐才可以越跑越快。成长的过程中总会遇到各式各样的问题，有些问题被我们忽视而过，有些问题我们选择了逃避，但其实我们还可以通过迎面应战、解决并反思的方式，在这样一次次战斗中快速地成长。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>每一个程序员都希望自己成为一个优秀的开发，实际上每个人对优秀的定义都不大一样。作为前端开发，除了专业能力以外，工作中还需要良好的表达与沟通能力。</p><p>如果我们还想继续往上走，通用计算机能力、架构能力、项目管理等能力也都需要提升。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;如果说基础知识的掌握是起跑线，那么使大家之间拉开差距的更多是前端项目开发经验和技能。对于一个项目来说，从框架选型和搭建，到项目维护、工程化和自动化、多人协作等各个方面，都需要我们在参与项目中不断地思考和改进，积累经验。&lt;/p&gt;
    
    </summary>
    
      <category term="前端技能提升" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E6%8A%80%E8%83%BD%E6%8F%90%E5%8D%87/"/>
    
    
      <category term="前端技能" scheme="https://godbasin.github.io/tags/%E5%89%8D%E7%AB%AF%E6%8A%80%E8%83%BD/"/>
    
  </entry>
  
  <entry>
    <title>技术方案的调研和设计过程</title>
    <link href="https://godbasin.github.io/2022/12/03/research-and-design-process/"/>
    <id>https://godbasin.github.io/2022/12/03/research-and-design-process/</id>
    <published>2022-12-03T11:18:23.000Z</published>
    <updated>2022-12-03T11:19:10.958Z</updated>
    
    <content type="html"><![CDATA[<p>技术方案设计属于架构能力中的一种，当我们开始作为某些功能/应用的 Owner 或是技术负责人来参与项目时，便会面临独立完成技术方案的调研和设计这样的工作内容。</p><a id="more"></a><p>一般来说，技术方案的调研和设计过程可以分为几个阶段：</p><ol><li>对项目的痛点、现状进行分析。</li><li>调用业界成熟的技术方案。</li><li>结合项目本身的现状和痛点，进行技术方案的选型和对比。</li></ol><h2 id="技术方案调研"><a href="#技术方案调研" class="headerlink" title="技术方案调研"></a>技术方案调研</h2><p>只有确保了技术方案的最优化、避免开发过程遇到问题需要推翻重做，从而能够快速落地并达成预期的效果。因此，在进行方案设计之前，对于项目存在的一些技术瓶颈、技术调整，我们需要先进行充分的前期调研。</p><p>在进行技术方案调研的时候，我们需要首先结合自身项目的背景、存在的痛点、现状问题进行分析，只有找到项目的问题在哪里，才可以更准确、彻底地去解决这些问题。</p><h3 id="分析项目背景，挖掘项目痛点"><a href="#分析项目背景，挖掘项目痛点" class="headerlink" title="分析项目背景，挖掘项目痛点"></a>分析项目背景，挖掘项目痛点</h3><p>技术方案的设计很多时候并不是命题作文，更多时候我们需要自己去挖掘项目的痛点，然后才是提出解决方案。</p><p>很多前端开发常常觉得自己做的项目没什么意思，认为每天都是重复的工作、繁琐的业务逻辑、糟糕的历史遗留代码。</p><p>实际上，那些会让我们觉得枯燥和重复的工作内容，也是可以去改善做好、并能从中获得成长的地方。好的业务可遇不可求，如果工作内容跟自己的预期不一样，我们就什么都不做了吗？</p><p>我们可以主动寻找项目存在的问题和痛点，并尝试去解决。不同的项目或是同一个项目的不同时期，关注的技术点都会不一样。对于一个前端项目来说，技术价值常常体现在系统性能、稳定性、可维护性、效率提升等地方，比如：</p><ul><li>对于用户量较大的项目，对系统稳定性要求较高，开发过程中需要关注是否会导致历史功能不兼容、是否会引入新的问题等；</li><li>对于大型复杂的项目，常常涉及多人协作，因此对系统可维护性要求更高，需要避免每次的改动都会导致性能和稳定性的下降，如何提升协作开发的效率等；</li><li>对于一次性的活动页面、管理端页面开发，技术挑战通常是如何提高开发效率，可以使用配置化、脚手架、自动化等手段来提升页面的开发和上线效率；</li></ul><p>找到项目的痛点之后，我们就可以进入项目的现状分析。</p><h3 id="现状分析"><a href="#现状分析" class="headerlink" title="现状分析"></a>现状分析</h3><p>项目的痛点可以转化为一个目标方向，比如：</p><ul><li>加载慢 -&gt; 首屏加载耗时优化</li><li>开发效率低 -&gt; 提升项目自动化程度</li><li>多人协作容易出问题 -&gt; 提升系统稳定性</li></ul><p>确定目标之后，我们就需要进行技术方案的设计，但很多时候由于项目现状存在的问题，一些技术优化的方案并不适用，需要进行方向的调整。</p><p>假设有一个同样规模大、成员多的小程序项目，由于该项目处于快速迭代的时期，考虑到投入产出比、产品形态也在不断调整，老板说“每个功能由开发自己保证”，决定不投入测试资源。</p><p>这意味着开发不仅需要在自测的时候确保核心用例的覆盖，同时也没有足够的排期来进行自动化测试（单元测试、集成测试、端到端测试等）的开发。</p><p>一般来说，我们还可以考虑建立用例录制和自动化回归的解决方案。比如开发一个浏览器插件，来获取用户操作的一些行为（比如 Redux 中的 Action 操作），将操作行为的页面结果（状态数据，比如 Redux 的 State）保存下来。在发布之前，可以通过自动化触发相同的操作行为，并与录制的页面结果进行比较，来进行回归测试。</p><p>但对于小程序的特殊性，我们无法让其运行在浏览器中，更无法获取到它的操作行为。在这样的情况下，还有什么办法可以保证系统的稳定性呢？</p><p>考虑到一个系统的上线过程包括开发、测试、灰度和发布四个阶段，如果无法通过测试阶段来及时发现问题，那么我们还可以通过灰度过程中来及时发现并解决问题。</p><p>比如，通过全埋点覆盖各个页面的功能，灰度过程中观察埋点曲线是否有异常、及时告警和排查问题、暂停灰度或者回滚等方式，来避免给更多的用户带来不好的体验。</p><p>通过灰度的方式来保证系统稳定性，会对局部的用户造成影响，这并不是一个最优的技术方案，它是考虑到项目的现状退而求其次的解决方案，但最终也同样可以达到提升系统稳定性这样一个目的。</p><p>当我们确定了技术优化的具体方向之后，便可以进行业界方案的调研阶段了。</p><h3 id="业界方案调研"><a href="#业界方案调研" class="headerlink" title="业界方案调研"></a>业界方案调研</h3><p>当我们遇到一些技术问题并尝试解决的时候，需要提醒自己，这些问题肯定有其他人也遇到过。为了避免技术方案的设计过于局限，我们可以进行前期的调研，找一些业界相对成熟的方案作为参考，分析这些方案的优缺点、是否适用于自己的项目中。</p><p>我们可以通过几种方式去进行业界方案的调研：</p><ol><li>与有相关经验的开发进行沟通，交流技术方案，提供参考思路。</li><li>参考其他系统对外公开的方案设计。</li><li>参考开源项目的源码设计。</li></ol><p>举个例子，对于交互复杂、规模大型的应用，要如何管理各个模块间的依赖关系呢？业界相对成熟的解决方案是使用依赖注入体系，其中著名的开源项目中有 Angular 和 VsCode 都实现了依赖注入的框架，我们可以通过研究它们的相关代码，分析其中的思路以及实现方式。</p><p>开源项目源码很多，要怎么才能找到自己想看的部分呢？带着疑问有目的性地看，会简单轻松得多。比如上述的依赖注入框架，我们可以带着以下的问题进行研究：</p><ol><li>依赖注入框架是什么？</li><li>模块是怎样初始化，什么时候进行销毁的？</li><li>模块是如何获取到其它模块呢？</li><li>模块间是如何进行通信的呢？</li></ol><p>通过这样的方式阅读源码，我们可以快速掌握自己需要的一些信息。在业界方案调研完成之后，我们需要结合自身项目进行具体的技术方案设计。</p><h2 id="技术方案设计"><a href="#技术方案设计" class="headerlink" title="技术方案设计"></a>技术方案设计</h2><p>技术方案设计过程中，我们需要根据上述的调研资料进行整理，包括项目痛点、现状、业界方案等，然后进行方案的选型和对比，最终给到适合项目的解决方案。</p><h3 id="方案选型-对比"><a href="#方案选型-对比" class="headerlink" title="方案选型/对比"></a>方案选型/对比</h3><p>业界的解决方案可能有多套，这时候我们需要对这些方案进行分析比较。</p><p>除此之外，如果需要投入人力和时间成本去做一件事，我们就会面临一个问题：如何让团队认同这件事情、并愿意给到资源让我去完成它呢？梳理项目现状和痛点、提供业界认可的案例参考、进行全面的方案对比和选型，也是一种方式。</p><p>例如，假设我们最近需要针对项目进行自动化性能测试能力的支持：</p><ul><li>项目现状：项目规模大、模块多、参与开发的成员也有几十人</li><li>项目痛点：经常因为一些不同模块的变更导致项目的性能下降却没法及时发现问题，往往是等到用户反馈或是某次开发、产品或者测试发现的时候才得知</li></ul><p>调研常见的一些性能分析方案，发现有几种方式：</p><ol><li>通过 Chrome Devtools 提供的 Performace 火焰图，来定位和发现问题，但这种方式局限于开发手动分析定位。</li><li>使用 Lighthouse，该工具可以提供初步的网页优化建议，也支持自动化。但 Lighthouse 本身更专注于短时间内对网站进行较全面的评估，存在像分析不够细致和深入这些问题。</li><li>使用 Chrome Devtools 提供的 Chrome Devtools Protocol（CDP）能力，进行自动化生成火焰图需要的 JSON。但业界对该 JSON 的分析工具几乎没有，大家都通过将该 JSON 传到 Chrome Devtools 提供的一个工具来还原火焰图，无法支持全程的自动化分析。</li></ol><p>其中，第一和第二种方案都无法从根本上解决遇到的问题。如果要彻底解决这个问题，可以考虑采取第三种方案，并打算通过自行研究分析 CDP（Chrome Devtools Protocol）生成的 JSON 来达到完全的自动化目的。</p><p>方案选型和对比是技术方案设计中重要的一个环节，可以将现状和痛点分析得更加全面，同时还可以避开一些其他人踩过的坑。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>在大多数工作中，对开发的要求都不仅限于实现功能。你是否有想过，如果只是编写代码，刚毕业的应届生花几周时间也一样能做到，那么我们的优势在哪里呢？</p><p>洞察工作中的瓶颈，并有足够的能力去设计方案、排期开发、解决并复盘，这些技能更能突显我们在岗位上的价值和能力。对团队来说，更需要这样能主动发现并解决问题的成员，而不是安排什么就只做什么的螺丝钉。</p><p>技术的发展都离不开知识的沉淀、分享和相互学习，当我们遇到一些问题不知道该怎么解决的时候，可以试着站到巨人的肩膀上，说不定可以看到更多。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;技术方案设计属于架构能力中的一种，当我们开始作为某些功能/应用的 Owner 或是技术负责人来参与项目时，便会面临独立完成技术方案的调研和设计这样的工作内容。&lt;/p&gt;
    
    </summary>
    
      <category term="前端技能提升" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E6%8A%80%E8%83%BD%E6%8F%90%E5%8D%87/"/>
    
    
      <category term="前端技能" scheme="https://godbasin.github.io/tags/%E5%89%8D%E7%AB%AF%E6%8A%80%E8%83%BD/"/>
    
  </entry>
  
  <entry>
    <title>前端性能优化--项目管理篇</title>
    <link href="https://godbasin.github.io/2022/11/20/front-end-performance-optimization-project/"/>
    <id>https://godbasin.github.io/2022/11/20/front-end-performance-optimization-project/</id>
    <published>2022-11-20T04:17:11.000Z</published>
    <updated>2022-11-20T04:17:50.733Z</updated>
    
    <content type="html"><![CDATA[<p>知晓要如何解决问题，只是真正解决问题的第一步。在工作里，我们更多时候遇到的问题不只是如何解决，而是如何有效落地。</p><a id="more"></a><p><a href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/">《前端性能优化–归纳篇》</a>中，我给大家介绍了很多常见的前端性能优化思路和方案，核心优化思想为时间上减少耗时、空间上降低资源占用。其中耗时优化在前端性能优化中更常见，优化方案包括网络请求优化、首屏加载优化、渲染过程优化、计算/逻辑运行提速四个方面。</p><p>性能优化通常需要投入不少的人力和成本来完成，因此更多的时候我们可以将其当作是一个项目的方式来进行管理。从项目管理的角度来讲，我们的性能优化工作会拆解为以下部分内容：</p><ol><li>确定优化的目标和预期。</li><li>确定技术方案。</li><li>项目排期和执行。</li><li>进行项目复盘。</li></ol><h2 id="1-确定优化的目标和预期"><a href="#1-确定优化的目标和预期" class="headerlink" title="1. 确定优化的目标和预期"></a>1. 确定优化的目标和预期</h2><p>性能优化的第一步，就是要确定优化的目标和预期。在给出具体的数据之前，我们首先需要对一些性能数据进行定义，常见包括：</p><ul><li>网络资源请求时间</li><li>Time To Start Render(TTSR)：浏览器开始渲染的时间</li><li>Dom Ready：页面解析完成的时间</li><li>Time To Interact(TTI))：页面可交互时间</li><li>Total Blocking Time (TBT)：总阻塞时间，代表页面处于不可交互状态的耗时</li><li>First Input Delay(FID)：从用户首次交互，到浏览器响应的时间</li></ul><p>要选择合适有效的指标进行定义，比如由于前端框架的出现，Page Load 耗时（<code>window.onload</code>事件触发的时间）已经难以用来作为页面可见时间的关键点，因此可以使用框架提供的生命周期，或者是使用 Largest Contentful Paint (LCP，关键内容加载的时间点)更为合适。</p><p>对需要关注的性能数据进行定义完成后，可以对它们进行目标和预期的确定，一般来说有两种方式：</p><ol><li>对比原先数据优化一定比例，比如 TTI 耗时减少 30%。</li><li>通过对竞品进行分析确定目标，比如比竞品耗时减少 20%。</li></ol><p>在确定了目标和预期之后，我们便可以根据预期来确定优化的方向、技术方案。</p><h2 id="2-确定技术方案"><a href="#2-确定技术方案" class="headerlink" title="2. 确定技术方案"></a>2. 确定技术方案</h2><p>根据确定的目标和预期，我们就可以选择合适的优化方案。</p><p>为什么不能将前面提到的全部技术方案都做一遍呢？显然这是不合理的。主要原因有两个：</p><ol><li>性价比。项目开发最看重的便是投入产出比，对于不同的项目来说，不同的技术优化方案需要投入人力不一样，很可能需要的投入较多但是优化效果可能不明显。</li><li>不适用，比如有些业务并不具备差异化服务。</li></ol><p>举个例子，阿猪的预期目标是客户端内打开应用 TTI 耗时减少 30%，因此他可以选择的优化方案包括：</p><ol><li>对首页数据进行分片/分屏加载。</li><li>首屏仅加载需要的资源，通过异步加载方式加载剩余资源。</li><li>使用服务端直出渲染。</li><li>使用 Tree-shaking 移除代码中无用的部分。</li><li>配合客户端进行资源预请求和预加载，比如使用预热 Web 容器。</li><li>配合客户端将资源和数据进行离线，可用于下一次页面的快速渲染。</li></ol><p>其中，5-6 需要客户端小伙伴进行支持，那么阿猪可以根据对方可以投入人力进行配合，来确定这两个优化点是否在本次方案中。</p><p>为了达成目标，对合适的技术优化点进行罗列之后，需要对每个优化点进行简单的调研，确定它们的优化效果。比如针对对首页数据进行分屏加载，可以通过简单的模拟测试，对比完整数据的 TTI 耗时，与首屏数据的 TTI 耗时，预估该技术点的优化效果如何。</p><p>最后，根据每个优化点的优化效果，以及相应的工作量评估，以预期为目标，选择性价比最优的技术方案。</p><p>在技术方案确定后，则需要对工作内容进行排期，并按计划执行。优化完成后，还需要结合目标和预期，对优化效果进行复盘，同时还可以提出未来优化的规划。</p><h2 id="3-项目排期和执行"><a href="#3-项目排期和执行" class="headerlink" title="3. 项目排期和执行"></a>3. 项目排期和执行</h2><p>这个步骤主要是排期实现，耗时最多。一般来说，需要注意的有两点：</p><ol><li>进行合理的分工排期。</li><li>对项目风险进行把控。</li></ol><h3 id="进行合理的分工排期"><a href="#进行合理的分工排期" class="headerlink" title="进行合理的分工排期"></a>进行合理的分工排期</h3><p>进行工作量评估的过程可以分为三步：</p><ol><li>确认技术方案，以及分工合作方式。</li><li>拆分具体功能模块，分别进行工作量评估，输出具体的排期时间表。</li><li>标注资源依赖情况和协作存在的风险，进行延期风险评估。</li></ol><p>当我们确认好技术方案之后，可以针对实现细节拆分具体的功能模块，分别进行工作量的预估和分工排期。具体的分工排期在多人协作的时候是必不可少的，否则可能面临分工不明确、接口协议未对齐就匆忙开工、最终因为各种问题而返工这些问题。</p><p>进行工作量评估的时候，可以精确到半天的工作量预期。对独自开发的项目来说，同样可以通过拆解功能模块这个过程，来思考具体的实现方式，也能提前发现一些可能存在的问题，并相应地进行规避。</p><p>提供完整的工作量评估和排期计划表（精确到具体的日期），可以帮助我们有计划地推进项目。在开发过程中，我们可以及时更新计划的执行情况，团队的其他人也可以了解我们的工作情况。</p><p>工作量评估和排期计划表的另外一个重要作用，是通过时间线去严格约束我们的工作效率、及时发现问题，并在项目结束后可针对时间维度进行项目复盘。</p><h3 id="对项目风险进行把控"><a href="#对项目风险进行把控" class="headerlink" title="对项目风险进行把控"></a>对项目风险进行把控</h3><p>我们在项目开发过程中，经常会遇到这样的情况：</p><ul><li>因为方案设计考虑不周，部分工作需要返工，导致项目延期</li><li>在项目进行过程中，常常会遇到依赖资源无法及时给到、依赖方因为种种原因无法按时支援等问题，导致项目无法按计划进行</li><li>团队协作方式未对齐，开发过程中出现矛盾，反复的争执和调整协作方式导致项目延期</li></ul><p>一个项目能按照预期计划进行，技术方案设计、分工和协作方式、依赖资源是否确定等，任何一各环节出现问题都可能导致整体的计划出现延误，这是我们不想出现的结果。</p><p>因此，我们需要主动把控各个环节的情况，及时推动和解决出现的一些多方协作的问题。</p><h2 id="4-进行项目复盘"><a href="#4-进行项目复盘" class="headerlink" title="4. 进行项目复盘"></a>4. 进行项目复盘</h2><p>很多开发习惯了当代码开发完成、发布上线之后就结束了这个项目，其实他们遗漏了一个很重要的环节：复盘。</p><p>我换过好多个团队，发现大多数团队和个人，都没有养成复盘的习惯。复盘是一个特别好的习惯，对于我们个人的成长也好，项目的优化和发展也好，都有很好的作用。</p><p>当然，也有一些人会把复盘当做背锅和甩锅，这是不对的。当我们在项目过程中，常常因为有 Deadline 而不断地赶节奏，大多数情况下都只能发现一个问题解决一个问题。而在项目结束之后，我们才可以跳出项目，做更加广视角下的回顾和思考。</p><p>有效的复盘，可以达到以下的效果：</p><ol><li>及时发现自己的问题并改进，避免掉进同一个坑。</li><li>让团队成员知道每个人都在做什么，团队管理不混乱。</li><li>整理沉淀和分享项目经验，让整个团队都得到成长。</li></ol><p>对于大多数开发来说，很多时候都不屑于主动邀功，觉得自己做了些什么老板肯定都看在眼里，写什么总结和复盘都是刷存在感的表现。实际上老板们每天的事情很多，根本没法关注到每一个人，我以前也曾经跟老板们问过这样一个问题：做和说到底哪个重要？</p><p>答案是两个都重要。把一件事做好是必须的，但将这件事分享出来，可以同样给团队带来更多的成长。</p><p>通过对项目进行复盘，除了可以让团队其他人和老板知道我们做了些什么，更重要的是，我们可以及时发现自身的一些问题并改进。</p><p>项目复盘最好可以结合数据来说话，性能优化的工作可以用具体的耗时和 CPU 资源占用这些指标来做总结，工具的开发可以用接入使用的用户数量来说明效果。甚至是普普通通的项目上线，也都可以使用对比排期和实际开发，复盘各个环节的耗时和质量。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>对于大部分前端开发来说，接触工具和框架开发、参与开源项目的机会比较少，很多时候我们写的都是“枯燥无聊”的业务代码。我们总认为只有做工具才会比较有意思、有技术挑战，很多时候会先入为主，认为业务代码写得再好也没用，也渐渐放弃了去思考要怎么把事情做好。</p><p>其实不只是工作中，我们生活里也可以常常进行反思和总结，这样我们的步伐才可以越跑越快。成长的过程中总会遇到各式各样的问题，有些问题被我们视而不见，有些问题我们选择了躲开，但其实我们还可以通过迎面应战、解决并反思的方式，在这样一次次战斗中快速地成长。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;知晓要如何解决问题，只是真正解决问题的第一步。在工作里，我们更多时候遇到的问题不只是如何解决，而是如何有效落地。&lt;/p&gt;
    
    </summary>
    
      <category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
    
    
      <category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>前端性能优化--SSR篇</title>
    <link href="https://godbasin.github.io/2022/10/15/front-end-performance-ssr/"/>
    <id>https://godbasin.github.io/2022/10/15/front-end-performance-ssr/</id>
    <published>2022-10-15T00:40:56.000Z</published>
    <updated>2022-10-15T00:59:51.765Z</updated>
    
    <content type="html"><![CDATA[<p>SSR 也算是前端性能优化中最常用的技术方案了，能有效地缩短页面的可见时间，给用户带来很好的体验。</p><a id="more"></a><p>我们常说的 SSR 指 Server-Side Rendering，即服务端渲染，属于首屏直出渲染的一种方案。</p><h1 id="SSR-性能优化"><a href="#SSR-性能优化" class="headerlink" title="SSR 性能优化"></a>SSR 性能优化</h1><p>首先，我们来看一下 SSR 方案主要优化了哪些地方的性能。</p><h2 id="SSR-渲染方案"><a href="#SSR-渲染方案" class="headerlink" title="SSR 渲染方案"></a>SSR 渲染方案</h2><p>一般来说，我们页面加载会分为好几个步骤：</p><ol><li>请求域名，服务器返回 HTML 资源。</li><li>浏览器加载 HTML 片段，识别到有 CSS/JavaScript 资源时，获取资源并加载。</li></ol><p>现在大多数前端页面都是单页面应用，使用了一些前端框架来渲染页面，因此还会有以下的流程：</p><ol start="3"><li>加载并初始化前端框架、路由库。</li><li>根据当前页面路由配置，命中对应的页面组件并进行渲染。</li><li>页面组件如果有依赖的资源，则发起请求获取数据后，再进行渲染。</li></ol><p>到这里，用户才完整地可见到当前页面的内容，并进行操作。可见，页面启动时的加载流程比较长，对应的耗时也都无法避免。</p><p>使用 SSR 服务端渲染，可以在第 1 步中直接返回当前页面的内容，浏览器可以直接进行渲染，再加载剩余的其他资源，因此优化效果是十分明显的。除了性能上的优化，SSR 还可以带来更好的 SEO 效果，因为搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。</p><p>那一般来说 SSR 技术方案要怎么做呢？其实从上面的过程中，我们也可以推导出，需要根据页面路由和页面内容生成对应的 HTML 内容，用于首次获取 HTML 的时候直接返回。</p><h3 id="框架自带-SSR-渲染"><a href="#框架自带-SSR-渲染" class="headerlink" title="框架自带 SSR 渲染"></a>框架自带 SSR 渲染</h3><p>现在我们大多数前端项目都会使用框架，而许多开源框架也提供了 SSR 能力。由于前端框架本身就负责动态拼接和渲染 HTML 的工作，因此实现 SSR 有天然的便利性。</p><p>以 Vue 为例子，Vue 提供了 <a href="https://ssr.vuejs.org/zh/" target="_blank" rel="noopener">vue-server-renderer</a> 服务端能力，基本思想基本也是前面说过的：浏览器请求服务端时，服务端完成动态拼接 HTML 的能力，将拼接好的 HTML 直接返回给浏览器，浏览器可以直接渲染页面：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 省略，可直接查看官网例子：https://ssr.vuejs.org/zh/guide/#%E5%AE%8C%E6%95%B4%E5%AE%9E%E4%BE%8B%E4%BB%A3%E7%A0%81</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 服务端收到请求时，生成 HTML 内容并返回</span></span><br><span class="line">server.get(<span class="string">"*"</span>, <span class="function">(<span class="params">req, res</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// 使用 Vue 实例</span></span><br><span class="line">  <span class="keyword">const</span> app = <span class="keyword">new</span> Vue(&#123;</span><br><span class="line">    data: &#123;</span><br><span class="line">      url: req.url,</span><br><span class="line">    &#125;,</span><br><span class="line">    template: <span class="string">`&lt;div&gt;访问的 URL 是： &#123;&#123; url &#125;&#125;&lt;/div&gt;`</span>,</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 使用 vue-server-renderer 将 Vue 实例生成最终的 HTML 内容</span></span><br><span class="line">  renderer.renderToString(app, context, <span class="function">(<span class="params">err, html</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="built_in">console</span>.log(html);</span><br><span class="line">    <span class="keyword">if</span> (err) &#123;</span><br><span class="line">      res.status(<span class="number">500</span>).end(<span class="string">"Internal Server Error"</span>);</span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    res.end(html);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">server.listen(<span class="number">8080</span>);</span><br></pre></td></tr></table></figure><p>当服务端收到请求时，生成 Vue 实例并依赖<code>vue-server-renderer</code>的能力，将 Vue 实例生成最终的 HTML 内容。该例子中，服务端直接使用现有资源就可以完成直出 HTML 的拼接.</p><p>但是在更多的前端应用场景下，通常还需要服务端动态获取其他的数据，才能完整地拼接出首屏需要的内容。一般来说，我们可以在服务端接到浏览器请求时，同时获取对应的数据，使用这些数据完成 HTML 拼接后再返回给浏览器。</p><p>在 Vue SSR 能力中，可以依赖<code>createApp</code>的能力，引入<code>Vuex</code>提前获取对应的数据并更新到 Store 中（参考<a href="https://ssr.vuejs.org/zh/guide/data.html" target="_blank" rel="noopener">数据预取和状态</a>），然后在服务端收到请求时，创建完整的 Vue 应用的能力：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> createApp = <span class="built_in">require</span>(<span class="string">"/path/to/built-server-bundle.js"</span>);</span><br><span class="line"></span><br><span class="line">server.get(<span class="string">"*"</span>, <span class="function">(<span class="params">req, res</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> context = &#123; url: req.url &#125;;</span><br><span class="line"></span><br><span class="line">  createApp(context).then(<span class="function">(<span class="params">app</span>) =&gt;</span> &#123;</span><br><span class="line">    renderer.renderToString(app, <span class="function">(<span class="params">err, html</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (err) &#123;</span><br><span class="line">        <span class="keyword">if</span> (err.code === <span class="number">404</span>) &#123;</span><br><span class="line">          res.status(<span class="number">404</span>).end(<span class="string">"Page not found"</span>);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">          res.status(<span class="number">500</span>).end(<span class="string">"Internal Server Error"</span>);</span><br><span class="line">        &#125;</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        res.end(html);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h3 id="同构-SSR-渲染"><a href="#同构-SSR-渲染" class="headerlink" title="同构 SSR 渲染"></a>同构 SSR 渲染</h3><p>前面我们讲到，Vue 提供了 SSR 的能力，这意味着我们可以使用 Vue 来完成客户端和服务端渲染，因此大部分的代码都可以复用。对于这种一份代码可分别在服务器和客户端上运行，我们成为“同构”。</p><p>对比自行实现 SSR 渲染，依赖开源框架提供的同构能力，一套代码可以分别实现 CSR 和 SSR，可大大节省维护成本。</p><p>还是以 Vue 为例，使用 Vue 框架实现同构，大概的逻辑如图：</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/786a415a-5fee-11e6-9c11-45a2cfdf085c.png" alt></p><p>不管是路由能力，还是组件渲染的能力，要保持同一套代码能分别运行在浏览器和服务端环境（Node.js）中，对于代码的编写则有一定的要求，比如 DOM 操作、window/document 对象等都需要谨慎，这些 <a href="https://ssr.vuejs.org/zh/guide/universal.html" target="_blank" rel="noopener">Vue 官方指引</a>也有介绍。</p><p>除此之外，服务端的入口逻辑显然会和客户端有差异，比如资源的获取方式、依赖的公共资源有所不一样等等。因此，在打包构建时会区分出两端的入口文件，并对通用逻辑做整合打包。这些内容也都在上面的图中有所体现。</p><h3 id="非同构-SSR-渲染"><a href="#非同构-SSR-渲染" class="headerlink" title="非同构 SSR 渲染"></a>非同构 SSR 渲染</h3><p>如果我们并没有强依赖前端框架，或是我们的项目过于复杂，此时可能要实现同构需要的成本比较大（抽离通用模块、移除环境依赖代码等）。考虑到项目的确需要 SSR 来加速页面可见，此时我们可以针对首屏渲染内容，自行实现 SSR 渲染。</p><p>SSR 核心思想前面也讲过好几遍了，因此要做的事情也比较明确：根据不同的路由，提供对于的页面首屏拼接的能力。由于不强依赖于同构，因此可以直接使用其他语言或是 ejs 来实现首屏 HTML 内容的拼接。</p><p>显然，非同构的方案实现 SSR 的成本，比同构的方案成本要高不少，并且还存在代码一致性、可维护性等一系列问题。因此，即使首屏直出的内容无法使用框架同构，大多数情况下，我们也会考虑尽量复用现有的代码，抽离核心的通用代码，并提供 SSR 服务代码编译打包的能力。</p><p>举个例子，假设我们的页面完全由 Canvas 进行渲染，显然 Canvas 是无法直出的。但正因为 Canvas 渲染前，需要加载的代码、计算渲染内容等各种流程过长，耗时较多，想要实现 SSR 渲染则可能只能考虑，针对首屏内容做一套 DOM/SVG 渲染用于 SSR。</p><p>基于这样的情况下，我们需要尽量复用计算部分的能力，抽离出通用的 Canvas/DOM/SVG 渲染接口，以尽可能实现对接口编程而不是对实现编程。</p><h2 id="SSR-利弊"><a href="#SSR-利弊" class="headerlink" title="SSR 利弊"></a>SSR 利弊</h2><p>上面主要围绕 SSR 的实现思想，介绍了开源框架 SSR、同构/非同构等 SSR 方案。</p><p>其实除了代码实现的部分以外，一个完整的 SSR 方案，还需要考虑：</p><ul><li>代码构建/部署：代码发布流程中，如何确保 SSR 部分代码的有效性，即不会因为非 SSR 部分代码的变更导致 SSR 服务异常</li><li>是否使用 Serverless：是否使用 Serverless 来部署 SSR 服务</li><li>是否使用缓存：是否可以将 SSR 部分或是最终生成的 HTML 结果进行缓存，节约服务端计算和拼接成本</li></ul><p>我们在选择一个技术方案的时候，不能只看它能带来什么收益，同时还需要评估一并带来的风险以及弊端。</p><p>对于 SSR 来说，收益是显而易见的，前面也有提到：</p><ul><li>实现更快的内容到达时间 (time-to-content)</li><li>更好的 SEO</li></ul><p>而其弊端也是客观存在的，包括：</p><ul><li>服务端资源消耗</li><li>方案需要开发成本和维护成本</li><li>可能会影响页面最终的完全可交互时间</li></ul><p>对于最后一点，有时候也会被我们忽略。因为 SSR 在最开始就提供了首屏完整的 HTML 内容，用户可见时间极大地提前了，我们常常会忘了关注页面所有功能加载完成、页面可交互的时间点。显然，由于浏览器需要在首屏时渲染完整的 HTML 内容，该过程也是需要一定的耗时的，所以后面的其他步骤完成的时间点都会有所延迟。如果首屏 HTML 内容很多/复杂的情况下，这种情况会更明显。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>SSR 的内容大概讲到这里，其实在更多的时候，SSR 方案的重点往往是文中一笔带过的弊端。实现一套同构渲染的代码，亦或是维护两套分别用于 CSR/SSR 的代码，这些方案的目的和方向都比较明确。</p><p>而 SSR 部署在什么环境、使用服务端还是 Serverless 生成，是否结合缓存实现、缓存更新策略又该是怎样的，如何保证非同构代码的渲染一致性，这些问题才是我们在将 SSR 方案落地过程中，需要反复思考和琢磨的问题。</p><p>我们在做方案调研的时候，也常常会过于关注开发成本和最终效果，从而忽略了整个项目和方案过程中的许多可能性。虽然目的的确很重要，但要记住过程也是很重要的。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;SSR 也算是前端性能优化中最常用的技术方案了，能有效地缩短页面的可见时间，给用户带来很好的体验。&lt;/p&gt;
    
    </summary>
    
      <category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
    
    
      <category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>前端这几年--15.关于互联网寒冬</title>
    <link href="https://godbasin.github.io/2022/09/17/about-front-end-15/"/>
    <id>https://godbasin.github.io/2022/09/17/about-front-end-15/</id>
    <published>2022-09-17T02:31:21.000Z</published>
    <updated>2022-09-17T02:33:53.007Z</updated>
    
    <content type="html"><![CDATA[<p>今年在互联网行业工作的大家，想必大概都有所感受。去年还在想方设法招聘应届生的许多公司，今年温度骤降，裁员的裁员，毁 offer 的毁 offer。</p><a id="more"></a><h2 id="所谓“冰河世纪”"><a href="#所谓“冰河世纪”" class="headerlink" title="所谓“冰河世纪”"></a>所谓“冰河世纪”</h2><p>今年来，听到了许多事、也看到了许多，看着不少认识的小伙伴一个个离开，万分感慨。</p><p>老板们强调着互联网寒冬，希望每个人都努力卷起来，最好能“当成自己的事业”来奋斗。因为冰河世纪，裁掉了好一部分人；因为冰河世纪，留下的每个人分到的事情更多；还因为冰河世纪，待遇都有所降低。</p><p>刚开始，不少人为了能留下来，的确变得更卷了。时间长了后，大部分人还是逐渐恢复了原本的节奏。毕竟要马跑，也得让马吃点草。</p><p>不少离开的人反而有了更好的去处，逃离了压抑的工作环境，同时还拿到了更好的待遇。这么一对比，其实有时候被迫脱离舒适的环境，其实也未必是件坏事，反而是帮我们下决心了。</p><h3 id="打铁还是得自身硬"><a href="#打铁还是得自身硬" class="headerlink" title="打铁还是得自身硬"></a>打铁还是得自身硬</h3><p>其实在所谓的互联网寒冬以前，我都十分重视个人的成长，因为温水煮青蛙是一件很危险的事情。不过相比于担心被淘汰，更多还是出于对自身的要求吧，我也挺喜欢不断成长的滋味的。</p><p>我一直认为，有能力的人走到哪里都不会担心。能力提升上去了，不会担心被淘汰，即使离开了也能很快找到下一份工作。因为有能力的人，哪里都缺。</p><p>在平时的工作里，其实的确能看到不少问题。这些问题我在之前的文章中也有所提过，比如工作方式是否过于流水线完成任务，比如是否有给自己留下足够的时间来思考和总结，比如是否过于关注得失而忽略了自身真正的成长，等等。</p><p>还是那句话，忙并不一定能有所成长，你需要花点时间偶尔复盘一下。</p><p>事情做得好不好，这也和不同的领导风格有关。有些老板喜欢给你安排事情的、不喜欢自作主张的，也有些老板喜欢你主动思考和提出更多解决方案的。</p><p>看来，打铁得自身硬的同时，遇到一个合得来的老板也挺重要的。</p><h3 id="关于影响力"><a href="#关于影响力" class="headerlink" title="关于影响力"></a>关于影响力</h3><p>当然，这并不是说被淘汰的都是能力不够的人。相反，我看到许多有潜力有能力的人离开了。除了个人选择之外，大部分原因便是归咎于其“影响力不够”。</p><p>其实我是十分讨厌影响力这个词的，因为它简陋又粗暴地描述大家的工作成果。是因为大家的工作成果无法被有效地量化，也无法确切地找出其中的问题，才会常常使用“影响力不够”这样的词语来概况总结。</p><p>但工作成果无法量化，更多时候是团队管理存在的问题。钻了空子的人，便会常常“刷脸”来提升自己的存在感，而遗憾的是，这样的操作常常会带来一定的效果。许多埋头苦干的人被忽略了，因为他们发声更少。</p><p>现实是，虽然我很讨厌影响力这种话，但实际上它常常就会在耳边响起。我也看到不少的小伙伴因为这种所谓的“影响力”在各种事情上被影响。因此，我还是建议大家，该表达的时候就要发声，这个浮躁的社会不会在乎你真正做了多少，他们只在乎他们看到了多少。</p><p>我不倡导过度地刷脸和表达，因此比较简单做到的是：发现问题积极响应、解决进展及时同步、风险及时同步。除此之外，自己的一些工作相关的想法其实也完全可以分享，比如提升工作效率的小技巧、解决某种问题的小技巧等等。</p><h3 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h3><p>其实有时候会想，这行业的领导还挺好当的。当然，好的老板很难当，但只是个老板的话，感觉还挺简单的，反正事情做好了都归功老板，出问题了底下人背锅。即使是在所谓的冰河世纪，裁员也会先裁底下员工，就算裁到 leader，也可以拿着大家做的成果出去轻松找到下一份工作。</p><p>虽然他们也常常说 leader 不好当，压力大事情多。但如果真的只是徒增压力和责任，没有其他收益，大概也不会那么多人争破头去抢这样的位置了吧。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;今年在互联网行业工作的大家，想必大概都有所感受。去年还在想方设法招聘应届生的许多公司，今年温度骤降，裁员的裁员，毁 offer 的毁 offer。&lt;/p&gt;
    
    </summary>
    
      <category term="工作这杯茶" scheme="https://godbasin.github.io/categories/%E5%B7%A5%E4%BD%9C%E8%BF%99%E6%9D%AF%E8%8C%B6/"/>
    
    
      <category term="分享" scheme="https://godbasin.github.io/tags/%E5%88%86%E4%BA%AB/"/>
    
  </entry>
  
  <entry>
    <title>前端性能优化--容器篇</title>
    <link href="https://godbasin.github.io/2022/08/14/front-end-performance-container/"/>
    <id>https://godbasin.github.io/2022/08/14/front-end-performance-container/</id>
    <published>2022-08-14T01:12:33.000Z</published>
    <updated>2022-08-14T01:15:43.533Z</updated>
    
    <content type="html"><![CDATA[<p>前面我们讲了很多前端应用内部的性能优化，实际上除了前端自身，我们还可结合容纳 Web 页面本身的客户端一起做优化。</p><a id="more"></a><p>首先，本文中提到的容器，基本上都是指 Web 页面的宿主，比如浏览器、APP 客户端、小程序，它们提供了 WebView 环境来运行 Web 应用。</p><h1 id="容器性能优化"><a href="#容器性能优化" class="headerlink" title="容器性能优化"></a>容器性能优化</h1><p>由于 Web 应用本身只运行在 WebView 中，而 WebView 的能力又依赖于宿主容器，因此 Web 应用本身很多能力都比较局限。如果宿主容器能配合一起做一些优化，效果要远胜于我们自身做的很多优化效果。</p><p>从性能优化的角度来说，宿主容器主要能提供的能力包括：</p><ul><li>加速页面打开</li><li>加速页面切换</li></ul><h2 id="加速页面打开"><a href="#加速页面打开" class="headerlink" title="加速页面打开"></a>加速页面打开</h2><p>对前端项目来说，我们常常会对首屏打开做很多的优化，包括尽量减少首屏需要的代码、对首屏渲染的内容进行分片等等（参考<a href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/">《前端性能优化–归纳篇》</a>）。</p><p>即使前端本身优化到极致，对于资源获取、请求数据等这些耗时占比较大的部分，还是存在的。但是如果容器能提供类似的能力，我们就可以将这部分的耗时做优化了，比如：</p><ul><li>提前下载并缓存 Web 相关资源，页面打开时直接获取缓存，比如 HTML/JavaScript/CSS</li><li>提前获取和缓存页面渲染相关的请求资源，页面请求时直接返回，或是直接从缓存中获取</li><li>提前启动 WebView 页面，并加载基础资源</li></ul><h3 id="资源准备"><a href="#资源准备" class="headerlink" title="资源准备"></a>资源准备</h3><p>我们可以在客户端即将打开某个 WebView 页面之前，提前将该页面资源下载下来，由此加快 WebView 页面加载的速度。</p><p>由于资源请求本身也会消耗一定的资源，一般来说会在比较明确使用的场景下才会使用。也就是说用户很可能会点进去该 WebView 页面，基于这样的前提来做资源准备，比如列表页进入详情页，比如底部 TAB 进入的页面等等。</p><p>这些提前下载并临时缓存的资源，可以包括：</p><ul><li>页面加载资源，包括 HTML/CSS/JavaScript 等</li><li>首屏页面内容的请求数据，比如分片数据的首片数据等</li></ul><p>资源预下载要做的时候相对简单，需要注意的是下载后的资源的管理问题，在使用完毕或是不需要的情况下需要及时的清理，如果过多的缓存会占用用户机器的资源。</p><p>其实除了依赖客户端，前端本身也有相关的技术方案，比如说可以使用 PWA 提前请求和缓存页面需要的资源。</p><h3 id="预加载"><a href="#预加载" class="headerlink" title="预加载"></a>预加载</h3><p>在需要的资源已经准备好的前提下，容器还可以提供预加载的能力，包括：</p><ul><li>容器预热：提前准备好 WebView 资源</li><li>资源加载：将已下载的 Web 资源进行加载，比如基础的 HTML/CSS/JavaScript 等资源</li></ul><p>举个例子，小程序中也有对资源预加载做处理。在小程序启动时，微信会为小程序展示一个固定的启动界面，界面内包含小程序的图标、名称和加载提示图标。此时，微信会在背后完成几项工作：下载小程序代码包、加载小程序代码包、初始化小程序首页。</p><p>小程序的启动过程也分了两个步骤：</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/wxapp-66.jpg" alt></p><ol><li>页面预渲染。这是准备 WebView 页面的过程，由于小程序里是双线程的设计，因此渲染层和逻辑层都会分别进行初始化以及公共库的注入。逻辑层和渲染层是并行进行的，并不会相互依赖和阻塞。</li><li>小程序启动。当用户打开小程序后，小程序开始下载业务代码，同时会在本地创建基础 UI（内置组件）。准备完成后，就会开始注入业务代码，启动运行业务逻辑。</li></ol><p>显然，小程序基础库和环境初始化相关的资源，都被提前内置在 APP 中了，并提前准备好相关的资源，使得用户打开小程序的时候，可以快速地加载页面。除此之外，小程序还提供了预加载的能力，业务方只需要配置提前拉取的资源，微信则可以在启动的过程中，提前将相关的资源拉取回来。</p><p>很多宿主预加载的方案也类似，比如对 WebView 页面做前置的资源下载和加载，当用户点击时尽快地给到用户体验。</p><h2 id="加速页面切换"><a href="#加速页面切换" class="headerlink" title="加速页面切换"></a>加速页面切换</h2><p>除了首次打开页面的加速，在页面切换时我们也可以做很多提速的事情。</p><h3 id="容器预热"><a href="#容器预热" class="headerlink" title="容器预热"></a>容器预热</h3><p>前面讲到，在打开小程序前，其实微信已经提前准备好了一个 WebView 层，由此减少小程序的加载耗时。</p><p>而当这个预备的 WebView 层被使用之后，一个新的 WebView 层同样地会被提前准备好。这样当开发者跳转到新页面时，就可以快速渲染页面了。这个过程也可以理解为容器的前置预热。</p><p>在这个例子中，小程序针对不同的页面使用了不同的 WebView 进行渲染，因此不管是首次打开，还是跳转/切换新页面，都会准备多一个 WebView 用来快速加载。</p><p>但多准备一个 WebView 本身也是对客户端的一种资源消耗，所以其实我们还可以考虑另外一种方案：容器切换。</p><h3 id="容器切换"><a href="#容器切换" class="headerlink" title="容器切换"></a>容器切换</h3><p>容器切换方案指当页面切换时复用同一个 WebView 资源，可以理解为前端单应用类似的方式在 APP 中做资源切换。</p><p>由于需要复用同一个 WebView，因此该方案对资源的管理要求较高，包括：</p><ul><li>对页面应用的生命周期管理完善，自顶向下实现初始化、更新和销毁的能力</li><li>页面切换时，需要及时清理原有逻辑和资源，比如定时器、页面遗留的 UI 和事件监听等</li><li>资源占用、内存泄露等问题，会随着 WebView 复用次数而积累</li></ul><p>要达到不同页面和前端应用之间的资源复用，要求比直接准备一个新的 WebView 容器要高很多。即使是不同的页面，也需要有统一的生命周期管理，约定好页面的一些销毁行为，并能执行到每个模块和组件中。</p><p>但如果项目架构和设计做得好，效果要远胜于容器预热，因为在进行页面切换的时候，很多资源可以直接复用，比如：</p><ul><li>通用的框架库，比如使用了 Vue/React 等前端框架、Antd 等组件库，就可以免去获取和加载这些资源的耗时</li><li>公共库的复用，项目中自行封装的一些工具库，也可以直接复用</li><li>模块复用，通用的模块比如顶部栏、底部栏、工具栏、菜单栏等功能，可以在页面切换时选择性保留，直接省略这部分模块的加载和页面渲染</li></ul><p>看到这里或许有些人会疑惑，如果是这样的话为什么不直接用单页面呢？要知道我们讨论的场景是客户端打开的场景，也就是说 WebView 页面的退出，大多数情况下是会先回到 APP 原生页面中。当用户进入到另外一个 WebView 页面时，才会重新打开 WebView，此时才考虑是用新预热的 WebView，还是直接复用刚才的 WebView。</p><p>总的来说，容器切换是一个设计要求高、副作用强、但优化效果好的方案。</p><h3 id="客户端直出渲染"><a href="#客户端直出渲染" class="headerlink" title="客户端直出渲染"></a>客户端直出渲染</h3><p>在有容器提供资源的基础上，我们还可以在 WebView 页面关闭前，对当前页面做截屏或是 HTML 保存处理。</p><p>在下一次用户进入到相同的页面中时，可以先使用上一次浏览的图片或是页面片段先预览，当页面加载完成后，再将预览部分移除。这种预加载（预览）的方案，由于是客户端提供的直出渲染能力，因此也被称为客户端直出渲染。</p><p>当然，相对于在页面关闭前保存，其实也可以直接实现直出渲染的能力，这样不管是否已经打开过某个页面，都可以通过容器预热时提前计算出直出渲染的内容，当页面打开时直接进行渲染。</p><p>这种方案有一个比较麻烦的地方：当缓存的页面内容发生变化时，需要及时更新直出渲染的内容。</p><p>因此，及时用户并不在页面内，也需要定期去获取最新的资源，并生成直出渲染的内容。当需要预渲染的页面多了，维护这些页面的实时性也需要消耗不少的资源，因此更适用于维护成本较低的页面。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>其实，容器的作用不只是加速页面打开速度，由于结合了原生 APP 的能力，我们甚至可以给 WebView 提供完整的离线加载能力。比如在网络离线的情况下，通过提前将资源下载并缓存，用户依然可以正常访问 APP 里的页面。</p><p>当然，每一项技术方案都是有利有弊，容器提供了更优的能力，也需要消耗一定的资源，我们可以结合自己项目本身的情况来做取舍。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;前面我们讲了很多前端应用内部的性能优化，实际上除了前端自身，我们还可结合容纳 Web 页面本身的客户端一起做优化。&lt;/p&gt;
    
    </summary>
    
      <category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
    
    
      <category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>前端性能优化--Canvas篇</title>
    <link href="https://godbasin.github.io/2022/07/09/front-end-performance-canvas/"/>
    <id>https://godbasin.github.io/2022/07/09/front-end-performance-canvas/</id>
    <published>2022-07-09T11:00:01.000Z</published>
    <updated>2022-07-09T11:03:51.017Z</updated>
    
    <content type="html"><![CDATA[<p>Canvas 渲染在前端应用中的使用场景不算多，但在大多数用到的场景下，也常常需要考虑性能瓶颈。</p><a id="more"></a><p>Canvas 的使用场景可能少一些（比如游戏、复杂图形、复杂排版等），本来想将 Canvas 渲染放在<a href="https://godbasin.github.io/2022/05/15/front-end-performance-render/">《前端性能优化——渲染篇》</a>一起介绍。后来想了下，Canvas 本身有许多优化点，可以结合自己在项目中的一些经验再详细地做介绍。</p><h1 id="Canvas-性能优化"><a href="#Canvas-性能优化" class="headerlink" title="Canvas 性能优化"></a>Canvas 性能优化</h1><p>其实对于 Canvas 的优化，<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas" target="_blank" rel="noopener">WDN</a> 上也有一些介绍。如果你在网上搜索相关内容，或许有许多的优化方向都和本文有些相像。</p><p>这是当然的，因为我们在做 Canvas 优化的时候，也同样会去找业界的方案做调研，结合自身项目的情况再做方案设计。</p><p>那么，这里整理下我了解到以及实践中的一些 Canvas 优化方案吧。</p><h2 id="Canvas-上下文切换"><a href="#Canvas-上下文切换" class="headerlink" title="Canvas 上下文切换"></a>Canvas 上下文切换</h2><p>Canvas 绘制 API 都是在上下文<code>context</code>上进行调用，<code>context</code>不是一个普通的对象，当我们对其赋值的时候，性能开销远大于普通对象。我们可以尝试将每个赋值操作执行一百万次，来看看其耗时：</p><table><thead><tr><th>赋值属性</th><th>耗时(ms)</th><th>耗时（非法赋值）(ms)</th></tr></thead><tbody><tr><td><code>font</code></td><td>200+</td><td>1500+</td></tr><tr><td><code>fillStyle</code></td><td>80+</td><td>800+</td></tr><tr><td><code>strokeStyle</code></td><td>50+</td><td>800+</td></tr><tr><td><code>lineWidth</code></td><td>30+</td><td>500+</td></tr></tbody></table><p>可见，频繁对 Canvas 上下文属性修改赋值是有一定的性能开销的。这是因为当我们调用<code>context.lineWidth = 2</code>时，浏览器会需要立刻地做一些事情，这样在下一次绘制的时候才能以最新的状态绘制。这意味着，在绘制两段不同字体大小的文本的时候，需要设置两次不同的字体，也就是需要进行两次<code>context</code>上下文状态的切换。</p><p>在大多数情况下，我们的 Canvas 绘制内容的样式不会太多。但是在绘制内容数量大、样式多的场景下，我们应该考虑如何减少上下文<code>context</code>的切换。</p><p>可以考虑使用先将相同样式的绘制内容收集起来，结合享元的方式将其维护起来。在绘制的时候，则可以针对每种样式做切换，切换后批量绘制相同样式的所有内容。</p><p>举个例子，我们绘制俄罗斯方块，可以考虑所有方块的信息收集起来，相同样式的放在一个数据中，切换上下文后遍历绘制。比如，边框信息放在一个数组中，背景色相同的放在一个数组中。</p><h2 id="Canvas-拆分"><a href="#Canvas-拆分" class="headerlink" title="Canvas 拆分"></a>Canvas 拆分</h2><p>一般来说，我们在 Canvas 里绘制的内容，都可以根据变更频率来拆分，简称动静分离。</p><p>Canvas 拆分的关键点在于：尽量避免进行不必要的渲染，减少频繁变更的渲染范围。</p><p>比如在游戏中，状态栏（血条、当前关卡说明等）相对动作/动画内容来说，这部分内容的变更不会太频繁，可以将其拆出到一个单独的 Canvas 来做绘制。再假设该游戏有个静态的复杂背景，如果我们每次更新内容都需要重新将这个背景再绘制一遍，显然开销也是不小的，那么这个背景我们也可以用单独的 Canvas 来绘制。</p><p>Canvas 拆分的前提是更新频率的内容分离，而在拆分的时候也有两个小技巧：</p><ol><li>根据绘制范围拆分。</li><li>根据堆叠层次关系拆分。</li></ol><h3 id="绘制范围的拆分"><a href="#绘制范围的拆分" class="headerlink" title="绘制范围的拆分"></a>绘制范围的拆分</h3><p>绘制范围的拆分要怎么理解呢？简单说就是将画布划分不同的区域，然后根据不同的区域更新频率，来进行 Canvas 拆分。</p><p>举个例子，假设我们现在需要实现 Web 端 VsCode，而整个界面都是由 Canvas 绘制（当然这样不大合理，这里假设只是为了更好地举例）。</p><p>我们可以简单地将 VsCode 拆分成几个区域：顶部栏、左侧栏、底部栏、编辑区。显然这个几个区域的变更频率、触发变更的前提都不一致，我们可以将其做拆分。</p><h3 id="堆叠层次的拆分"><a href="#堆叠层次的拆分" class="headerlink" title="堆叠层次的拆分"></a>堆叠层次的拆分</h3><p>如果说绘制范围的拆分是二维角度，那么堆叠层次更像是三维的 y 轴方向的拆分。</p><p>前面提到的游戏画布拆分，其实背景图片便是堆叠在其余内容的下面。我们可以考虑更复杂的场景，比如我们要实现 Web 版的 Excel/Word，那么我们也可考虑按照堆叠顺序来做拆分：背景色、文字、边框线等等。</p><p>对于有堆叠顺序的绘制来说，Canvas 拆分的优化效果更好。因为如果是二维角度的内容，我们可以只擦除和重绘某个 x/y 轴范围的内容就可以。</p><p>但是涉及到绘制内容的堆叠，如果不做 Canvas 的拆分，意味着我们其中任何一个层级的内容变更，都需要将所有层级的内容擦除并且重绘。比如在 Excel 场景下，某个区域的格子背景颜色变更，我们需要将该区域的格子全部擦除，再重新分别绘制背景色、文字、边框线、其他内容等等。</p><p>实际上，结合前面提到的<code>context</code>上下文的性能开销可知，我们在绘制的时候，很可能并不是以单个格子为单位来进行顺序堆叠的绘制，而是整个画布所有格子一起做顺序绘制（意思是，先绘制所有格子的背景色，再绘制所有格子的文字和边框线等等）。</p><p>在这样的情况下，如果没有做 Canvas 堆叠顺序的拆分，意味着每一个小的变更，我们都需要将整个表格的内容进行重绘。</p><h3 id="Canvas-拆分的开销"><a href="#Canvas-拆分的开销" class="headerlink" title="Canvas 拆分的开销"></a>Canvas 拆分的开销</h3><p>需要注意的是，Canvas 本身的维护也会存在一定的开销，并不是说我们拆的越多越好。</p><p>可以根据项目的实际情况，结合 Canvas 拆离后的效果，确定 Canvas 拆分的最终方案。</p><h2 id="离屏渲染"><a href="#离屏渲染" class="headerlink" title="离屏渲染"></a>离屏渲染</h2><p>对于离屏渲染的概念，大多数情况是指：使用一个不可见（或是屏幕外）的 Canvas 对即将渲染的内容的某部分进行提前绘制，然后频繁地将屏幕外图像渲染到主画布上，避免重复生成该部分内容的步骤。</p><p>比如，提前绘制好某个图像，在画布更新的时候直接使用该图像：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 在离屏 canvas 上绘制</span></span><br><span class="line"><span class="keyword">var</span> canvasOffscreen = <span class="built_in">document</span>.createElement(<span class="string">"canvas"</span>);</span><br><span class="line">canvasOffscreen.width = dw;</span><br><span class="line">canvasOffscreen.height = dh;</span><br><span class="line">canvasOffscreen</span><br><span class="line">  .getContext(<span class="string">"2d"</span>)</span><br><span class="line">  .drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在绘制每一帧的时候，绘制这个图形</span></span><br><span class="line">context.drawImage(canvasOffscreen, x, y);</span><br></pre></td></tr></table></figure><h3 id="各种离屏渲染场景"><a href="#各种离屏渲染场景" class="headerlink" title="各种离屏渲染场景"></a>各种离屏渲染场景</h3><p>关于离屏渲染，其实结合不同的使用场景，还可以达到不同的效果。比如：</p><p>(1) 使用离屏 Canvas 提前绘制特定内容。</p><p>这就是前面说到的提前绘制好需要的内容，避免每次重复生成的开销。</p><p>(2) 使用双 Canvas 交替绘制。</p><p>考虑 Canvas 滚动的场景，比如分页绘制，离屏 Canvas 可以提前绘制下一页/下一屏的内容，在切换的时候可以直接使用提前绘制好的内容。</p><p>通过这样的方式，可以加快 Canvas 的绘制，可以理解为预渲染的效果。</p><p>(3) 使用 OffscreenCanvas 达到真正的离屏。</p><p>通过 OffscreenCanvas API，真正地将离屏 Canvas 完整地运行在 worker 线程，有效减少主线程的性能开销。</p><h3 id="OffscreenCanvas-API-能力"><a href="#OffscreenCanvas-API-能力" class="headerlink" title="OffscreenCanvas API 能力"></a>OffscreenCanvas API 能力</h3><p>要达到将 Canvas 运行在 web worker 线程中，需要依赖 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/OffscreenCanvas" target="_blank" rel="noopener">OffscreenCanvas API</a> 提供的能力。</p><blockquote><p>需要注意的是，该 API 同样可以运行在主线程中。即使是在主线程中运行，其开销也比普通 Canvas 要小。</p></blockquote><p><code>OffscreenCanvas</code>提供了一个可以脱离屏幕渲染的 Canvas 对象，可运行在在窗口环境和 web worker 环境。但是该 API 已知具有兼容性问题（比如 Safari 和 IE，以及部分安卓 Webview），需要考虑不兼容情况下的降级方案。关于此能力现有的技术方案和文档较少，可参考：</p><ul><li><a href="https://zhuanlan.zhihu.com/p/34698375" target="_blank" rel="noopener">OffscreenCanvas - 概念说明及使用解析</a></li><li><a href="https://developers.google.com/web/updates/2018/08/offscreen-canvas" target="_blank" rel="noopener">OffscreenCanvas — Speed up Your Canvas Operations with a Web Worker</a></li></ul><p>对于该 API，核心的优势在于：当主线程繁忙时，依然可以通过 OffscreenCanvas 在 worker 中更新画布内容，避免给用户造成页面卡顿的体验。</p><p>除此之外，还可以进一步考虑在兼容性支持的情况下，通过将局部计算运行在 worker 中，减少渲染层的计算耗时，提升渲染层的渲染性能。</p><h2 id="其他-Canvas-优化方式"><a href="#其他-Canvas-优化方式" class="headerlink" title="其他 Canvas 优化方式"></a>其他 Canvas 优化方式</h2><p>上面介绍了几种较大的 Canvas 优化方案，实际上我们在项目中还需要考虑：</p><ul><li>做内容的增量更新渲染，避免频繁地绘制大范围的内容</li><li>避免浮点数的坐标点，浏览器为了达到抗锯齿的效果会做额外的运算，建议用整数取而代之</li><li>使用 CSS transform 代替 Canvas 计算缩放（CSS transforms 使用 GPU，因此速度更快）</li><li>过于复杂的计算逻辑，可以考虑做任务的拆分，避免长时间计算造成页面卡顿</li></ul><p>这里简单提一下增量渲染。</p><h3 id="增量渲染"><a href="#增量渲染" class="headerlink" title="增量渲染"></a>增量渲染</h3><p>增量渲染需要对内容的变更做计算，将变更的内容局限在某个特定范围，从而避免频繁地绘制大范围的内容。</p><p>举个例子，假设我们的画布内容支持向下滚动，那么我们在滚动的时候可以考虑：</p><ul><li>根据滚动的距离，将上一帧可复用的内容做裁剪保存</li><li>在下一帧绘制中，先将上一帧中重复的内容在新的位置绘制</li><li>原有内容绘制完成后，新增的部分内容再进行重新绘制</li></ul><p>通过这样的方式，可以节省掉一部分的内容绘制和生成过程，提升每次渲染的速度。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>使用 Canvas 绘制，我们则脱离了浏览器自身的绘制过程，因此更加要注意性能问题，避免卡顿和耗时较大的计算。</p><p>至于耗时长的计算和卡顿的优化，我会在另外一篇文章中做详细的介绍（参见<a href="https://godbasin.github.io/2022/06/04/front-end-performance-no-responding/">前端性能优化——卡顿篇</a>）。</p><blockquote><p>我有一个游戏梦，Canvas 做游戏应该也很好玩吧。</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Canvas 渲染在前端应用中的使用场景不算多，但在大多数用到的场景下，也常常需要考虑性能瓶颈。&lt;/p&gt;
    
    </summary>
    
      <category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
    
    
      <category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>前端性能优化--卡顿篇</title>
    <link href="https://godbasin.github.io/2022/06/04/front-end-performance-no-responding/"/>
    <id>https://godbasin.github.io/2022/06/04/front-end-performance-no-responding/</id>
    <published>2022-06-04T12:36:25.000Z</published>
    <updated>2022-06-04T12:37:54.609Z</updated>
    
    <content type="html"><![CDATA[<p>如果页面中存在耗时较长的计算任务，那么卡顿也是需要关注的一个性能优化点。</p><a id="more"></a><p>前面我有给大家整体地讲过<a href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/">《前端性能优化–归纳篇》</a>，其实里面已经囊括了大多数场景下的一些性能优化的方向。</p><p>当我们开始讨论卡顿时，往往意味着页面中有较大的逻辑运算，该计算任务耗时太长，阻塞了浏览器的主线程，导致用户的一些操作无法及时响应。因此，我们今天卡顿优化的重点在于如何优化耗时较长的计算。</p><h1 id="卡顿优化"><a href="#卡顿优化" class="headerlink" title="卡顿优化"></a>卡顿优化</h1><p>还是那句话，对于大多数的渲染场景，我们都可以使用浏览器的 Performance 来录制和分析性能问题，Performance 适用于针对某个具体、可复现的问题做分析。</p><p>卡顿问题同样也是，我们可以在火焰图中看到一些长耗时的任务，然后再逐个分析具体的耗时问题出现在哪里，逐一解决。</p><p>这里介绍一些耗时任务的优化方案。</p><h2 id="赋值和取值"><a href="#赋值和取值" class="headerlink" title="赋值和取值"></a>赋值和取值</h2><p>其实大多数情况下，我们都很少会去在意一些变量的取值和赋值。</p><p>但是在一些复杂的计算场景下，比如深层次的遍历中，需要考虑的点就很多很细，比如：</p><ul><li>尽量将不需要执行的逻辑前置，提前判断做<code>return</code></li><li>减少<code>window</code>对象或是深层次对象上的取值，可以将其保存为临时变量使用</li><li>减少不必要的遍历，<code>Array.filter()</code>这种语法也是一次遍历，需要注意</li><li>对复杂数据结构的数据查询，可以考虑优化数据结构</li></ul><p>一些简单的问题，在重复上百万次的计算之后，都会被无数放大。即使是从<code>window</code>对象上获取某个值，然后做计算生成 DOM 这样的操作，如果将它放在多层遍历的最里层去做，同样会造成性能问题。</p><p>如果你的项目中有使用 Canvas，且重度依赖画布绘制，你会发现 ctx 的上下文切换开销也不低，后面也会单独对 Canvas 的一些性能问题做补充说明。</p><p>这也告诉我们，平时的代码习惯也要好，比如副作用、全局对象等，都可以考虑做更好的设计。</p><h2 id="优化计算性能-内存"><a href="#优化计算性能-内存" class="headerlink" title="优化计算性能/内存"></a>优化计算性能/内存</h2><p>除了上面提到的一些基础场景（比如取值赋值），很多时候我们提升计算性能，还依赖于使用更好的算法和数据结构。</p><p>其实大多数时候，前端都很少涉及到算法和数据结构相关的设计，但是在极端复杂的场景下，也需要考虑做一些优化。</p><p>讲一个经典例子，在 VSCode 的 1.21 发布版本中包含了一项重大改进：<a href="https://code.visualstudio.com/blogs/2018/03/23/text-buffer-reimplementation" target="_blank" rel="noopener">全新的文本缓冲区实现</a>，在内存和速度方面都有大幅的性能提升。</p><p>在这次优化中，VSCode 引入了红黑树的数据结构，替代了原有的线性阵列，优化了内存避免了内存爆炸，同时也优化了查询的时间复杂度。</p><p>其实，除了计算耗时过长，如果出现内存占用过多的情况下，同样会造成浏览器频繁的 GC。如果你有仔细观察 Performance，便会发现浏览器的 GC 本身也需要不小的耗时。</p><p>所以，我们还需要时常关注内存情况，考虑：</p><ul><li>使用享元的方式来优化数据存储，减少内存占用</li><li>及时地清理不用的资源，比如定时器</li><li>避免内存泄露等问题</li></ul><h2 id="大任务拆解"><a href="#大任务拆解" class="headerlink" title="大任务拆解"></a>大任务拆解</h2><p>对于一些计算耗时较长的任务，我们可以考虑将任务做拆解，分成一个个的小任务，做异步执行。</p><p>比如，考虑将任务执行耗时控制在 50 ms 左右。每执行完一个任务，如果耗时超过 50 ms，将剩余任务设为异步，放到下一次执行，给到页面响应用户操作和更新渲染的时间。</p><p>我们都知道 React 框架有使用虚拟 DOM 的设计。实际上，虽然虚拟 DOM 解决了页面被频繁更新和渲染带来的性能问题，但传统虚拟 DOM 依然有以下性能瓶颈：</p><ul><li>在单个组件内部依然需要遍历该组件的整个虚拟 DOM 树</li><li>在一些组件整个模版内只有少量动态节点的情况下，这些遍历都是性能的浪费</li><li>递归遍历和更新逻辑容易导致 UI 渲染被阻塞，用户体验下降</li></ul><p>对此，React 中还设计了协调器（Reconciler）与渲染器（Renderer）来优化页面的渲染性能。而在 React16 中，还新增了调度器（Scheduler）。</p><p>调度器能够把可中断的任务切片处理，能够调整优先级，重置并复用任务。调度器会根据任务的优先级去分配各自的过期时间，在过期时间之前按照优先级执行任务，可以在不影响用户体验的情况下去进行计算和更新。通过这样的方式，React 可在浏览器空闲的时候进行调度并执行任务。</p><p>这便是将大任务做拆解方案中，很好的一个例子。</p><h2 id="其他计算优化"><a href="#其他计算优化" class="headerlink" title="其他计算优化"></a>其他计算优化</h2><p>除了上述的一些优化方案，我们还可以考虑：</p><p>(1) 使用 Web Worker。</p><p>如今 Web Worker 已经是前端应用中比较常用的一个能力了，对于一些耗时较长、相对独立的计算任务，我们可以使用 Web Worker 来进行计算。</p><p>当然，由于这些计算任务已经不在主线程了，那么通信的耗时、数据的同步、Worker 兼容性等问题也需要考虑，做好兜底和兼容方案，保证核心能力的使用。</p><p>(2) 使用 WebAssembly。</p><p>WebAssembly 的运行性能接近原生，因此在许多计算耗时的场景上会被使用来优化，比如文件上传、文件/视频内容识别等等。</p><p>(3) 使用 AOT 技术。</p><p>使用 AOT 技术，通过将计算过程提前，减少计算等待时长。</p><p>举个例子，在 Angular 框架中，提供了预编译（AOT）能力，无须等待应用首次编译，以及通过预编译的方式移除不需要的库代码、减少体积，还可以提早检测模板错误。</p><h1 id="卡顿的监控和定位"><a href="#卡顿的监控和定位" class="headerlink" title="卡顿的监控和定位"></a>卡顿的监控和定位</h1><p>出现卡顿问题的时候，往往难以定位，因为这个时候页面常常已经卡死，无法做更多的调试操作。</p><h2 id="Performance"><a href="#Performance" class="headerlink" title="Performance"></a>Performance</h2><p>定位一个页面的运行是否有卡顿，最简单又直接的方式是录制 Performance。Performance 会把耗时长的任务直接标记为红色，我们可以根据这些任务，查找和分析具体产生耗时的脚本是哪些，然后去做优化。</p><p>但是，Performance 仅对开发者来说比较方便，在真实用户的使用场景里，未必有条件能提供 Performance 的录制。更多的时候，我们只能粗略地监控用户的卡顿情况，发现这样的场景，并尝试去解决。</p><h2 id="requestAnimationFrame"><a href="#requestAnimationFrame" class="headerlink" title="requestAnimationFrame"></a>requestAnimationFrame</h2><p>一般来说我们监控卡顿，可以考虑使用<code>window.requestAnimationFrame</code>方法。该方法会在绘制下一帧绘制前被调用，这意味着当前的同步计算任务即将结束。</p><p>前面也有说到，卡顿大多数是因为长耗时的计算任务导致的。那么，我们就可以考虑在某个函数执行之前记下时间戳，而在<code>window.requestAnimationFrame</code>的时候再取其中的时间差，判断当前函数的执行耗时是否合理。</p><p>当然，该方案并不是完全准确，因为我们常常会在一个函数中间调用另外一个函数，还可能会同步抛出事件通知，执行其他的计算任务。</p><p>不过，考虑到真实的线上用户里无法直接使用 Performance，这也算是一个能做卡顿监控的方案。我们可以配合日志、其他不同的监控和上报等，来做更多的问题定位。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>大多数的卡顿场景，都是由于页面渲染掉帧导致的。因此针对页面的更新渲染，不管是 DOM 渲染还是 Canvas 渲染，需要注意将帧率保持在 50~60 FPS 的范围内，这样用户的体验会流程很多。</p><p>当然，如果我们的代码里写了死循环，造成页面直接卡死了，也是卡顿的一种情况，但这就又是另外一个故事了。</p><blockquote><p>愿天下所有的开发同学不再遇到卡顿~</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;如果页面中存在耗时较长的计算任务，那么卡顿也是需要关注的一个性能优化点。&lt;/p&gt;
    
    </summary>
    
      <category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
    
    
      <category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>前端性能优化--渲染篇</title>
    <link href="https://godbasin.github.io/2022/05/15/front-end-performance-render/"/>
    <id>https://godbasin.github.io/2022/05/15/front-end-performance-render/</id>
    <published>2022-05-15T05:46:21.000Z</published>
    <updated>2022-05-15T05:49:51.417Z</updated>
    
    <content type="html"><![CDATA[<p>对于内容复杂和变更频繁的前端应用，页面渲染也常常是性能优化的核心场景。</p><a id="more"></a><p>前面我有给大家整体地讲过<a href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/">《前端性能优化–方案归纳篇》</a>，其实里面已经囊括了大多数场景下的一些性能优化的方向。关于加载流程相关的优化，也有在<a href="https://godbasin.github.io/2022/04/09/front-end-performance-startup/">《前端性能优化–加载流程篇》</a>一文中进行详细的介绍。</p><p>本文主要围绕页面渲染相关的内容，来进行性能优化分析。</p><h1 id="首屏渲染"><a href="#首屏渲染" class="headerlink" title="首屏渲染"></a>首屏渲染</h1><p>说到页面渲染，首屏的渲染显然是最首要的。其实前面在归纳篇也有介绍，首屏加载优化核心点在于：<strong>将页面内容尽快展示给用户，减少页面白屏时间。</strong></p><p>首屏渲染包括了首屏内容的加载和渲染两个过程。</p><h2 id="首屏内容加载"><a href="#首屏内容加载" class="headerlink" title="首屏内容加载"></a>首屏内容加载</h2><p>对于首屏加载过程，我们可以通过以下方式进行优化：</p><ul><li>使用骨架屏进行预渲染</li><li>对页面进行分片/分屏加载，将页面可见/可交互时间提前</li><li>优化资源加载的顺序和粒度，仅加载需要的资源，通过异步加载方式加载剩余资源</li><li>使用差异化服务，比如读写分离，对于不同场景按需加载所需要的模块</li><li>使用服务端直出渲染，减少页面二次请求和渲染的耗时</li><li>使用秒看技术，通过预览的方式（比如图片）提前将页面内容提供给用户</li><li>配合客户端进行资源预请求和预加载，比如使用预热 Web 容器</li><li>配合客户端将资源和数据进行离线，可用于下一次页面的快速渲染</li></ul><p>这里提到了很多的方向，但是否每个优化点都适用于自身的项目中，需要结合项目本身做调研和验证。举个简单的例子，最后两条优化点明显是基于有自研客户端的前提下，需要配合客户端一起做优化才可以实现。</p><p>实际上，对于首屏内容的优化，前端开发在项目中更常用的点是骨架屏、数据分片/分屏加载、SSR DOM 直出渲染这几种，因为这几个优化点相对来说方向明确、效果明确、实现相对简单。如果是想要对项目做差异化服务、做资源的拆分和优化，则可能随着项目的复杂度增加，方案难度提升、实现成本也增长。</p><h2 id="首屏内容渲染"><a href="#首屏内容渲染" class="headerlink" title="首屏内容渲染"></a>首屏内容渲染</h2><p>对于首屏内容渲染的过程，更多时候我们是指浏览器渲染 HTML 的过程。该过程可以优化的点也是我们常常提及的，浏览器渲染页面的优化过程，比如：</p><ul><li>将 CSS 放在<code>&lt;head&gt;</code>里，可用来避免浏览器渲染的重复计算</li><li>将 JavaScript 脚本放在<code>&lt;body&gt;</code>的最后面，避免资源阻塞页面渲染</li><li>减少 DOM 数量，减少浏览器渲染过程中的计算耗时</li><li>通过合理使用浏览器 GPU 合成，提升浏览器渲染效率</li></ul><p>以上这些，是我们在做首屏渲染时考虑渲染过程的优化点。虽然这些优化点属于前端基础和共识，也常常会出现在基础面试中。</p><p>很多时候我们为了准备面试而学习了很多的知识和原理，却容易在将知识和实践结合的过程中忘记。越是基础和简单的点，反而往往会在实际写代码的时候被忽略，直到性能出现了问题，这些基础的优化点才会被注意到。</p><p>当然，首屏性能的提升，除了渲染相关的，也还有上一篇我们提到的<a href="https://godbasin.github.io/2022/04/09/front-end-performance-startup/">加载流程相关的优化</a>。</p><h1 id="页面更新"><a href="#页面更新" class="headerlink" title="页面更新"></a>页面更新</h1><p>除了首屏内容需要尽快加载和渲染以外，当页面内容需要更新的时候，我们也需要尽可能地减少更新内容渲染的耗时。</p><p>一般来说，页面更新场景我们常常会关注用户操作和页面渲染。</p><h2 id="用户操作"><a href="#用户操作" class="headerlink" title="用户操作"></a>用户操作</h2><p>页面内容的更新，一般有两种情况：</p><ol><li>用户自身操作（点击、输入、拖拽等）的页面响应。</li><li>实时内容的变更（比如聊天室的消息提醒、弹幕等等）。</li></ol><p>如果是用户自身的操作，则我们需要及时地更新页面内容，让用户感受到操作生效了。该过程应该是优先级最高的，一般需要同步进行。因为如果有别的任务在执行而导致主线程阻塞，就容易造成页面卡顿的体验。关于卡顿相关的，我会另外再起一篇文章介绍，这里就不过多展开啦。</p><p>至于实时内容的变更，优先级更多会比用户操作稍微低一些，也基本上都是异步进行的。我们还可以考虑对变更内容做合并、批量更新，也可以考虑定时拉取最新内容更新的方式。</p><h3 id="事件委托"><a href="#事件委托" class="headerlink" title="事件委托"></a>事件委托</h3><p>对于用户交互频繁的场景，我们还得注意事件的绑定。相信很多人都了解过事件委托，如果在列表数量内容较大的时候，对成千上万节点进行事件监听，也是不小的性能消耗。使用事件委托的方式，通过将事件绑定在父元素上，我们可以大量减少浏览器对元素的监听，也是在前端性能优化中比较简单和基础的一个做法。</p><p>事件委托是很常见的优化方式，需要注意的是，如果我们直接在<code>document.body</code>上进行事件委托，可能会带来额外的问题。由于浏览器在进行页面渲染的时候会有合成的步骤，合成的过程会先将页面分成不同的合成层，而用户与浏览器进行交互的时候需要接收事件。</p><p>如果我们在<code>document.body</code>上被绑定了事件，这时候整个页面都会被标记。即使我们的页面不关心某些部分的用户交互，合成器线程也必须与主线程进行通信，并在每次事件发生时进行等待。此时可以使用<code>passive: true</code>选项来解决。</p><h2 id="页面渲染"><a href="#页面渲染" class="headerlink" title="页面渲染"></a>页面渲染</h2><p>我们在页面内容更新的时候，一般也可以考虑以下优化点：</p><ul><li>减少/合并 DOM 操作，减少页面更新的内容范围，减少浏览器渲染过程中的计算耗时</li><li>对于页面动画，可以使用 CSS transition 能力，减少 DOM 属性的修改</li><li>使用资源预加载，在空闲时间，提前将用户可能需要用到的资源进行获取并加载（比如下一页的内容）</li></ul><h3 id="DOM-操作合并"><a href="#DOM-操作合并" class="headerlink" title="DOM 操作合并"></a>DOM 操作合并</h3><p>说到 DOM 操作的合并和减少，目前大多数前端框架都提供了虚拟 DOM 的能力（比如 Vue 和 React）。虚拟 DOM 本身就有对 DOM 操作和更新做优化，通过使用 JavaScript 对象模拟 DOM 元素，并在页面需要更新时对更新的部分做 DOM Diff，尽可能地减少内容的更新频率和范围。</p><p>虽然现在大多数前端项目都离不开前端框架，也正因为这些框架本身已经做了很多的优化，所以我们常常会忘记和忽略掉这些注意事项。</p><p>但也从侧面论证了，即使是很基础的优化点也需要重视，即使是简单的优化点也可以做出很棒的设计。</p><h3 id="页面滚动渲染"><a href="#页面滚动渲染" class="headerlink" title="页面滚动渲染"></a>页面滚动渲染</h3><p>考虑到页面滚动的场景，可能会出现性能问题的地方常常是长列表/页面的渲染。</p><p>由于页面内容过多，页面的 DOM 元素数量也很多，容易造成页面渲染的卡顿。在这样的情况下，我们可以考虑仅渲染可见区域的部分，比如页面内容超出滚动范围之外，就可以进行销毁，将页面的 DOM 数量保持在一定范围内。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>本文主要围绕页面渲染和更新的过程，介绍了一些性能优化的方向。其实如果你有注意到，就会发现本文的内容大多数还是基础和简单的前端知识点。</p><p>还是那句话，前端基础和原理知识基本上大多数开发都掌握了，但是要怎么将这些知识在项目中发挥到最佳的作用呢？这才是我们工作中在不断探索和学习，获得经验和成长的关键点。</p><p>纸上得来终觉浅，了解一些知识很简单，但是要深入理解、熟练掌握后，再结合自身经验将它发挥出来，才是其价值的完整体现。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;对于内容复杂和变更频繁的前端应用，页面渲染也常常是性能优化的核心场景。&lt;/p&gt;
    
    </summary>
    
      <category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
    
    
      <category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>前端性能优化--加载流程篇</title>
    <link href="https://godbasin.github.io/2022/04/09/front-end-performance-startup/"/>
    <id>https://godbasin.github.io/2022/04/09/front-end-performance-startup/</id>
    <published>2022-04-09T13:53:02.000Z</published>
    <updated>2022-05-15T05:49:38.864Z</updated>
    
    <content type="html"><![CDATA[<p>对于前端应用的性能优化，大多数时候我们都是从加载流程开始优化起。</p><a id="more"></a><p>前面我有给大家整体地讲过<a href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/">《前端性能优化–归纳篇》</a>，其实里面已经囊括了大多数场景下的一些性能优化的方向。</p><p>越是交互复杂、用户量大的业务，对性能的要求就越是严格。大多数的前端性能优化，都是从页面的启动和加载流程开始梳理和定位，对于功能复杂的业务来说，这样的梳理尤为重要。</p><blockquote><p>注意：前面说过性能优化分为时间和空间两个角度，本文中提及的性能优化更多是指时间角度（即耗时）的优化。</p></blockquote><h1 id="常见的页面加载流程"><a href="#常见的页面加载流程" class="headerlink" title="常见的页面加载流程"></a>常见的页面加载流程</h1><p>其实我们在性能优化的归纳篇有简单说过，页面加载的过程其实跟我们常常提起的浏览器页面渲染流程几乎一致：</p><ol><li>网络请求，服务端返回 HTML 内容。</li><li>浏览器一边解析 HTML，一边进行页面渲染。</li><li>解析到外部资源，会发起 HTTP 请求获取，加载 Javascript 代码时会暂停页面渲染。</li><li>根据业务代码加载过程，会分别进入页面开始渲染、渲染完成、用户可交互等阶段。</li><li>页面交互过程中，会根据业务逻辑进行逻辑运算、页面更新。</li></ol><p>那么，我们可以针对其中的每个步骤做优化，主要包括：资源获取、资源加载、页面可见、页面可交互。</p><h2 id="资源获取"><a href="#资源获取" class="headerlink" title="资源获取"></a>资源获取</h2><p>资源获取主要可以围绕两个角度做优化：</p><ul><li>资源大小</li><li>资源缓存</li></ul><h3 id="资源大小"><a href="#资源大小" class="headerlink" title="资源大小"></a>资源大小</h3><p>一般来说，前端都会在打包的时候做资源大小的优化，资源类型包括 HTML、JavaScript、CSS、图片等。优化的方向包括：</p><p>(1) 合理的对资源进行分包。</p><p>首次渲染时只保留当前页面渲染需要的资源，将可以异步加载、延迟加载的资源拆离。通常我们会在代码编译打包的时候做处理，比如<a href="https://webpack.docschina.org/guides/code-splitting/" target="_blank" rel="noopener">使用 Webpack 将代码拆到不同的 bundle 包中</a>。</p><p>(2) 移除不需要的代码。</p><p>我们项目中常常会引入许多开源代码，同时我们自己也会实现很多的工具方法，但是实际上并不是全部相关的代码都是最终需要执行的代码，所以我们可以在打包的时候移除不需要的代码。现在基本大多数的打包工具都提供了类似的能力，比如 Tree-shaking。</p><p>除此之外，如果我们的项目较大，使用和依赖了多个不同的仓库。如果在不同的代码仓库里，都依赖了同样的 npm 代码包，那么我们可能会遇到打包时引入多次同样的 npm 包的情况。一般来说，我们在管理依赖包的时候，可以使用<code>peerDependency</code>来进行管理，避免多次安装依赖、以及版本不一致导致的多次打包和安装等情况。</p><p>(3) 资源压缩和合并。</p><p>代码压缩也常常是在打包阶段进行的，包括 JavaScript 和 CSS 等代码，在一些情况下也可以使用图片合并（雪碧图的生成）。通常也是使用的打包工具以及插件自带的压缩能力，开启压缩后的代码可能比较难定位，可以配合 Sorce Mapping 来进行问题定位。</p><p>除了打包时的压缩，我们在页面加载的时候也可以启用 HTTP 的 gzip 压缩，可以减少资源 HTTP 请求的耗时。</p><h3 id="资源缓存"><a href="#资源缓存" class="headerlink" title="资源缓存"></a>资源缓存</h3><p>资源缓存的优化，其实更多时候跟我们的资源获取的链路有关，包括：</p><ul><li>减少 DNS 查询时间，比如使用浏览器 DNS 缓存、计算机 DNS 缓存、服务器 DNS 缓存</li><li>合理地使用 CDN 资源，有效地减少网络请求耗时</li><li>对请求资源进行缓存，包括但不限于使用浏览器缓存、HTTP 缓存、后台缓存，比如使用 Service Worker、PWA 等技术</li></ul><p>其实，我们观察资源获取的链路，获取除了大小和缓存的角度以外，还可以做更多的优化，比如：</p><ul><li>使用 HTTP/2、HTTP/3，提升资源请求速度</li><li>对请求进行优化，比如对多个请求进行合并，减少通信次数</li><li>对请求进行域名拆分，提升并发请求数量</li></ul><h2 id="资源加载"><a href="#资源加载" class="headerlink" title="资源加载"></a>资源加载</h2><p>资源加载步骤中，我们一般也有以下的优化角度：</p><ul><li>加载流程拆分</li><li>资源懒加载</li><li>资源预加载</li></ul><h3 id="加载流程拆分"><a href="#加载流程拆分" class="headerlink" title="加载流程拆分"></a>加载流程拆分</h3><p>页面的加载过程，常常分为两个阶段：页面可见、页面可交互。</p><p>前面我们讲了对资源做拆分，在页面启动加载的时候仅加需要的资源，拆分的过程则可以结合上述的两个阶段来做处理。</p><p>(1) 页面可见。</p><p>页面可见可以分为部分可见以及内容完全可见。</p><p>对于部分可见，一般来说可以做 loading 的展示或是直出，让用户知道页面正在加载中，而非无响应。</p><p>对于内容完全可见，则是用户可视区域内的内容完全渲染完毕。除此之外，当前可视范围以外的内容，则可以拆离出首屏的分包，通过预加载或是懒加载的方式进行异步加载。</p><p>(2) 页面可交互。</p><p>同样的，页面可交互也可以分为部分可交互以及完全可交互。</p><p>一般来说，组件的样式渲染仅需要 HTML 和 CSS 加载完成即可，而组件的功能则可能需要加载具体的功能代码。对于复杂或是依赖资源较多的功能，加载的耗时可能相对较长。在这样的情况下，我们可以选择将该部分的资源做异步加载。</p><p>在初始的内容加载完毕之后，剩下的资源需要延迟加载。对于页面功能完全可交互，同样依赖于分包资源延迟加载。加载流程的优化，不管是页面可见，还是页面可交互，都离不开延迟加载。</p><p>延迟加载可分为两种方式进行加载：懒加载和预加载。因此，资源懒加载和预加载也是加载流程中很重要的一部分。</p><h3 id="资源懒加载"><a href="#资源懒加载" class="headerlink" title="资源懒加载"></a>资源懒加载</h3><p>我们常说的懒加载其实又被称为按需加载，顾名思义就是需要用到的时候才会进行加载。通过将非必要功能进行懒加载的方式，可以有效地减少页面的初始加载速度，提升页面加载的性能。</p><p>常见的场景比如某些组件在渲染时不具备完整的功能，当用户点击的时候，才进行对应逻辑的获取和加载。遇到点击时未加载完成的情况下，可以通过适当的方式提示用户功能正在加载中。</p><p>资源懒加载常常也是跟资源分包一起进行，大多数前端框架（比如 Vue、React、Angular）也都提供了懒加载的能力，也可以<a href="https://webpack.docschina.org/guides/lazy-loading/" target="_blank" rel="noopener">配合 Webpack 打包</a>做处理。</p><h3 id="资源预加载"><a href="#资源预加载" class="headerlink" title="资源预加载"></a>资源预加载</h3><p>资源预加载也称为闲时加载，很多时候我们可以在页面空闲的时候，对一些用户可能会用到的资源做提前加载，以加快后续渲染或者操作的时间。</p><p>仔细一看，资源预加载和资源懒加载都比较相似，都会通过将资源拆离的方式做成异步延迟的方式加载。两者的区别在于：</p><ul><li>懒加载的功能只会在需要的时候才进行加载，因为一些功能用户可能不会使用到，比如帮助中心、反馈功能等等</li><li>预加载的功能则是在不阻塞核心功能的时候，尽可能利用空闲的资源提前加载，这部分的功能则是用户很可能会使用到，比如获取下一屏页面的内容数据</li></ul><h1 id="复杂场景下的加载流程"><a href="#复杂场景下的加载流程" class="headerlink" title="复杂场景下的加载流程"></a>复杂场景下的加载流程</h1><p>在页面到达可交互状态之后，后续的加载流程也可以根据业务场景做后续的优化。对于一些复杂的业务，我们可以结合业务的特点做更进一步的性能优化。</p><h2 id="复杂加载流程管理"><a href="#复杂加载流程管理" class="headerlink" title="复杂加载流程管理"></a>复杂加载流程管理</h2><p>对于页面初始化流程过于复杂的应用来说，我们可以对加载流程做任务的拆分，分阶段地进行加载。</p><p>举个例子，假设我们需要在 Web 端加载 VsCode，那么我们可能需要考虑以下各个功能的加载：</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">- 整体页面框架</span><br><span class="line">- 顶部菜单栏</span><br><span class="line">- 左侧工具栏</span><br><span class="line">- 底部状态栏</span><br><span class="line">- 文件目录栏</span><br><span class="line">- 文件详情</span><br><span class="line">  - 内容展示</span><br><span class="line">  - 编辑功能</span><br><span class="line">  - 菜单功能</span><br><span class="line">- 搜索功能</span><br><span class="line">- 插件功能</span><br></pre></td></tr></table></figure><p>以上只是我按照自己想法粗略拆分的功能，我们可以简单分成几个加载阶段：</p><ol><li>页面整体框架加载完成。此时可以看到各个功能区域的分布，包括顶部菜单栏、左侧工具栏、底部状态栏、项目内容区域等等，但这些区域的内容未必都完全加载完成。</li><li>通用功能加载完成。比如顶部菜单栏、左侧工具栏、底部状态栏等等，一些具体的菜单或是工具的功能可以做按需加载和预加载，比如搜索功能。</li><li>项目内容相关框架加载完成。此时可以看到项目相关的内容区域，比如文件目录、当前文件的内容详情等等。</li><li>插件功能。用户安装的插件，在核心功能都加载完成之后再获取和加载。</li></ol><p>当我们根据项目的具体加载过程做了阶段划分之后，则可以将我们的代码做任务拆分，可以拆分成串行和并行的任务。串行的任务比如按照阶段划分的大任务，并行的任务则可以是某个阶段内的小任务，其中也可以包括一些异步执行的任务，或是延迟加载的任务。</p><h2 id="长耗时任务的拆离"><a href="#长耗时任务的拆离" class="headerlink" title="长耗时任务的拆离"></a>长耗时任务的拆离</h2><p>如果我们的应用中会有耗时较长的计算任务，比如拉取回来的数据需要计算处理后才能渲染，那么我们可以对这些耗时较长的任务做任务拆分。</p><p>同样的，我们还是回到 Web 端加载 VsCode 的场景。假设我们在加载某个特别大的文件，则可以考虑分别对该文件的内容获取、数据转换做任务拆分，比如分片获取该文件的内容，根据分片的内容做渲染的计算，计算过程如果耗时较长，也可以做异步任务的拆分，甚至可以结合 Web Worker 和 WebAssembly 等技术做更多的优化。</p><h2 id="读写分离"><a href="#读写分离" class="headerlink" title="读写分离"></a>读写分离</h2><p>对于交互复杂、需要加载的资源较多的情况下，如果用户的权限只是可读，那么对于编辑相关的功能可以做资源拆离，对于有权限的用户才进行编辑能力的加载。</p><p>读写分离其实属于资源拆分的一种具体场景，我们可以结合业务的具体场景做具体的功能拆分，比如管理员权限相关的管理功能，也是类似的优化场景。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>我们做性能优化的场景，更多时候出现在我们的应用出现了性能瓶颈的时候。大多数情况下，前端应用都相对简单，也无需做过度的优化。</p><p>对于复杂的应用，对加载流程和链路的梳理、划分，不管是对我们做架构设计来说，还是对于做性能优化来说，都有不小的帮助。只有理清楚整个应用的加载流程，结合对每个步骤和阶段的耗时统计，我们可以针对性地对耗时较长的地方做优化。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;对于前端应用的性能优化，大多数时候我们都是从加载流程开始优化起。&lt;/p&gt;
    
    </summary>
    
      <category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
    
    
      <category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>前端性能优化--归纳篇</title>
    <link href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/"/>
    <id>https://godbasin.github.io/2022/03/06/front-end-performance-optimization/</id>
    <published>2022-03-06T03:24:13.000Z</published>
    <updated>2022-04-09T13:55:48.299Z</updated>
    
    <content type="html"><![CDATA[<p>对于前端开发来说，性能优化老生常谈了。不管是日常工作中，还是涉及到晋级答辩，性能都是频繁被我们提及的一个话题。</p><p>性能优化不是一劳永逸的解决方案，项目在发展过程，代码不断地迭代和变更。我们在某个阶段优化过的代码，过段时间性能又会慢慢下降，这也是前端开发常把性能挂在嘴边的原因。</p><a id="more"></a><p>当页面加载时间过长、交互操作不流畅时，会给用户带来很糟糕的体验。越是使用时间越长的产品，用户对体验的要求越高，如果出现卡顿或是加载缓慢，最坏的情况下会导致用户的流失。</p><p>对于性能优化，其实解决方案也比较常见和通用了，但是基本上也只有指导思想，实施起来还得具体项目具体分析。</p><h1 id="常见的性能优化方案"><a href="#常见的性能优化方案" class="headerlink" title="常见的性能优化方案"></a>常见的性能优化方案</h1><p>对于前端应用来说，网络耗时、页面加载耗时、脚本执行耗时、渲染耗时等耗时情况会影响用户的等待时长，而 CPU 占用、内存占用、本地缓存占用等则可能会导致页面卡顿甚至卡死。</p><p>因此，性能优化可以分别从<strong>耗时和资源占用</strong>两方面来解决，我个人也比较喜欢将其称为“时间”和“空间”两个维度。</p><h2 id="时间角度优化：减少耗时"><a href="#时间角度优化：减少耗时" class="headerlink" title="时间角度优化：减少耗时"></a>时间角度优化：减少耗时</h2><p>我们知道浏览器在页面加载过程中，会进行以下的步骤：</p><ul><li>网络请求相关（发起 HTTP 请求从服务端获取页面资源，包括 HTML/CSS/JS/图片资源等）</li><li>浏览器解析 HTML 和渲染页面</li><li>加载 Javascript 代码时会暂停页面渲染（包括解析到外部资源，会发起 HTTP 请求获取并加载）</li></ul><p>在浏览器的首次加载和渲染完成之后，不代表用户就可以马上交互和操作。根据业务代码加载过程，页面还会分别进入页面开始渲染、渲染完成、用户可交互等阶段。除此之外，页面交互过程中，会根据业务逻辑进行逻辑运算、页面更新。</p><blockquote><p>题外话：为什么我们常常说要理解原理呢？性能优化便是个很好的例子，如果你不知道这个过程具体发生了什么，就很难找到地方下手去进行优化。</p></blockquote><p>根据这个过程，我们可以从四个方面进行耗时优化：</p><ol><li>网络请求优化。</li><li>首屏加载优化。</li><li>渲染过程优化。</li><li>计算/逻辑运行提速。</li></ol><p>在前端性能优化实践中，网络请求优化和首屏加载优化方案使用频率最高，因为不管项目规模如何、各个模块和逻辑是否复杂，这两个方向的耗时优化方案都是比较通用的。相比之下，对于页面内容较多、交互逻辑/运算逻辑复杂的项目，才需要针对性地进行渲染过程优化和计算/逻辑运行提速。</p><p>一起来看看~</p><h3 id="1-网络请求优化"><a href="#1-网络请求优化" class="headerlink" title="1. 网络请求优化"></a>1. 网络请求优化</h3><p>网络请求优化的目标在于减少网络资源的请求和加载耗时，如果考虑 HTTP 请求过程，显然我们可以从几个角度来进行优化：</p><ol><li>请求链路：DNS 查询、部署 CDN 节点、缓存等。</li><li>数据大小：代码大小、图片资源等。</li></ol><p>对于请求链路，核心的方案常常包括使用缓存，比如 DNS 缓存、CDN 缓存、HTTP 缓存、后台缓存等等，前端的话还可以考虑使用 Service Worker、PWA 等技术。使用缓存并非万能药，很多使用由于缓存的存在，我们在功能更新修复的时候还需要考虑缓存的情况。除此之外，还可以考虑使用 HTTP/2、HTTP/3 等提升资源请求速度，以及对多个请求进行合并，减少通信次数；对请求进行域名拆分，提升并发请求数量。</p><p>数据大小则主要考对请求资源进行合理的拆分（CSS、Javascript 脚本、图片/音频/视频等）和压缩，减少请求资源的体积，比如使用 Tree-shaking、代码分割、移除用不上的依赖项等。</p><p>在请求资源返回后，浏览器会进行解析和加载，这个过程会影响页面的可见时间，通过对首屏加载的优化，可有效地提升用户体验。</p><h3 id="2-首屏加载优化"><a href="#2-首屏加载优化" class="headerlink" title="2. 首屏加载优化"></a>2. 首屏加载优化</h3><p>首屏加载优化核心点在于两部分：</p><ol><li>将页面内容尽快地展示给用户，减少页面白屏时间。</li><li>将用户可操作的时间尽量提前，避免用户无法操作的卡顿体验。</li></ol><p>减少白屏时间除了我们常说的首屏加载耗时优化，还可以考虑使用一些过渡的动画，让用户感知到页面正在顺利加载，从而避免用户对于白屏页面或是静止页面产生烦躁和困惑。除了技术侧的优化，很多时候产品策略的调整，给用户带来的体验优化效果不低于技术手段优化，因此我们也需要重视。</p><p>整体的优化思路包括：尽可能提前页面可见，以及将用户可交互的时间提前。一般来说，我们需要尽可能地降低首屏需要的代码量和执行耗时，可以通过以下方式进行：</p><ul><li>对页面的内容进行分片/分屏加载</li><li>仅加载需要的资源，通过异步或是懒加载的方式加载剩余资源</li><li>使用骨架屏进行预渲染</li><li>使用差异化服务，比如读写分离，对于不同场景按需加载所需要的模块</li><li>使用服务端直出渲染，减少页面二次请求和渲染的耗时</li></ul><p>有些时候，我们的页面也需要在客户端进行展示，此时可充分利用客户端的优势：</p><ul><li>配合客户端进行资源预请求和预加载，比如使用预热 Web 容器</li><li>配合客户端将资源和数据进行离线，可用于下一次页面的快速渲染</li><li>使用秒看技术，通过生成预览图片的方式提前将页面内容提供给用户</li></ul><p>除了首屏渲染以外，用户在浏览器页面过程中，也会触发页面的二次运算和渲染，此时需要进行渲染过程的优化。</p><h3 id="3-渲染过程优化"><a href="#3-渲染过程优化" class="headerlink" title="3. 渲染过程优化"></a>3. 渲染过程优化</h3><p>渲染过程的优化要怎么定义呢？我们可以将其理解为首屏加载完成后，用户的操作交互触发的二次渲染。</p><p>主要思路是减少用户的操作等待时间，以及通过将页面渲染帧率保持在 60FPS 左右，提升页面交互和渲染的流畅度。包括但不限于以下方案：</p><ul><li>使用资源预加载，提升空闲时间的资源利用率</li><li>减少/合并 DOM 操作，减少浏览器渲染过程中的计算耗时</li><li>使用离屏渲染，在页面不可见的地方提前进行渲染（比如 Canvas 离屏渲染）</li><li>通过合理使用浏览器 GPU 能力，提升浏览器渲染效率（比如使用 css transform 代替 Canvas 缩放绘制）</li></ul><p>以上这些，是对常见的 Web 页面渲染优化方案。对于运算逻辑复杂、计算量较大的业务逻辑，我们还需要进行计算/逻辑运行的提速。</p><h3 id="4-计算-逻辑运行提速"><a href="#4-计算-逻辑运行提速" class="headerlink" title="4. 计算/逻辑运行提速"></a>4. 计算/逻辑运行提速</h3><p>计算/逻辑运行速度优化的主要思路是“拆大为小、多路并行”，方式包括但不限于：</p><ul><li>通过将 Javscript 大任务进行拆解，结合异步任务的管理，避免出现长时间计算导致页面卡顿的情况</li><li>将耗时长且非关键逻辑的计算拆离，比如使用 Web Worker</li><li>通过使用运行效率更高的方式，减少计算耗时，比如使用 Webassembly</li><li>通过将计算过程提前，减少计算等待时长，比如使用 AOT 技术</li><li>通过使用更优的算法或是存储结构，提升计算效率，比如 VSCode 使用红黑树优化文本缓冲区的计算</li><li>通过将计算结果缓存的方式，减少运算次数</li></ul><p>以上便是<strong>时间</strong>维度的性能优化思路，还有<strong>空间</strong>维度的资源优化情况。</p><h2 id="空间角度优化：降低资源占用"><a href="#空间角度优化：降低资源占用" class="headerlink" title="空间角度优化：降低资源占用"></a>空间角度优化：降低资源占用</h2><p>提到性能优化，大多数我们都在针对页面加载耗时进行优化，对资源占用的优化会更少，因为资源占用常常会直接受到用户设备性能和适应场景的影响，大多数情况下优化效果会比耗时优化局限，因此这里也只能说一些大概的思路。</p><p>资源占用常见的优化方式包括：</p><ul><li>合理使用缓存，不滥用用户的缓存资源（比如浏览器缓存、IndexDB），及时进行缓存清理</li><li>避免存在内存泄露，比如尽量避免全局变量的使用、及时解除引用等</li><li>避免复杂/异常的递归调用，导致调用栈的溢出</li><li>通过使用数据结构享元的方式，减少对象的创建，从而减少内存占用</li></ul><p>说到底，我们在做性能优化的时候，其实很多情况下会依赖时间换空间、空间换时间等方式。性能优化没有银弹，只能根据自己项目的实际情况做出取舍，选择相对合适的一种方案去进行优化。</p><p>对于页面耗时和资源占用的性能优化分析，大部分情况都可以使用 Chrome 开发者工具进行针对性的分析和优化。</p><h3 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h3><p>实际上，除了遇到问题的时候进行优化，更优的方案是在工作流中搭建一个监控性能指标的步骤，每次变更发布前都跑一遍，发现性能下降之后进行及时的告警，推动开发者解决。对于这块，之前我也有简单描述过，可以参考<a href="https://godbasin.github.io/front-end-playground/front-end-basic/deep-learning/front-end-performance-analyze.html">《补齐 Web 前端性能分析的工具盲点》</a>一文。</p><p>对于性能优化，其实本文只整理和归纳了一些常见的思路，至于实际上在项目中要怎么处理和使用，等有空的时候我再来跟大家讲一下~~</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;对于前端开发来说，性能优化老生常谈了。不管是日常工作中，还是涉及到晋级答辩，性能都是频繁被我们提及的一个话题。&lt;/p&gt;
&lt;p&gt;性能优化不是一劳永逸的解决方案，项目在发展过程，代码不断地迭代和变更。我们在某个阶段优化过的代码，过段时间性能又会慢慢下降，这也是前端开发常把性能挂在嘴边的原因。&lt;/p&gt;
    
    </summary>
    
      <category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
    
    
      <category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>前端这几年--答辩晋级这件事</title>
    <link href="https://godbasin.github.io/2022/02/27/about-updating/"/>
    <id>https://godbasin.github.io/2022/02/27/about-updating/</id>
    <published>2022-02-27T07:31:34.000Z</published>
    <updated>2022-02-27T07:39:55.933Z</updated>
    
    <content type="html"><![CDATA[<p>最近又是答辩季，程序员最讨厌写的 PPT 又到了不得不写的时候了。之前也有在帮一些小伙伴做准备，所以顺便给大家分享一些晋级答辩的思考和技巧吧～</p><a id="more"></a><p>关于答辩晋级这个内容，最开始我是直接做的视频放在 B 站分享，参考<a href="https://www.bilibili.com/video/BV1tu411X7qn/" target="_blank" rel="noopener">《程序员日志–晋级答辩这件事》</a>。</p><blockquote><p>关于做视频和写文章，感觉自己从最初的只会写文章，到现在已经慢慢也会做一些视频了。视频的表达和文章相差很远，我自己的感受是，对于需要反复阅读、技术深度的内容，还是更适合用文章来记录。而视频更适合写一些需要录屏和讲解的模式，加上 PPT 本身的结构化，更容易去给大家梳理清楚逻辑架构，但是很多细节就很难讲清楚了。</p></blockquote><h2 id="如何对待晋级答辩这件事"><a href="#如何对待晋级答辩这件事" class="headerlink" title="如何对待晋级答辩这件事"></a>如何对待晋级答辩这件事</h2><p>对于很多大公司的程序员来说，晋级答辩关乎着是否可以升职加薪，而答辩成功与否常常会对个人的工作态度和心态造成较大的影响。</p><p>我个人的看法是：<strong>认真对待它，但不要过分依赖它。</strong></p><p>这句话怎么理解呢？如果你仔细观察身边其他同事，大多数会分成两类：</p><ol><li>平时工作只是完成工作本身，不希望受到答辩影响，但是到答辩的时候却临时抱佛脚。</li><li>过分看重答辩，在平日工作里就抢一些方便答辩的活，如果没有的话甚至自己造各种轮子，而不在乎这些轮子是否合适。</li></ol><p>以上两种态度都可以改善，我们可以在平时就认真地把手上的每件事做好，而到了答辩的时候也要认真地对待，但是不要因为答辩这件事影响了自己原本该有的工作态度和对待项目质量的要求。</p><p>或许有些人会疑惑，造不合适的轮子，为什么答辩能通过呢？</p><p>其实答辩这件事，本身也有认知偏差和主观因素。由于陈述内容是由答辩人自身提供的，所以很多时候都会只把好的一面呈现出来，而使用的技术栈或是造的一些轮子给原有项目造成的影响，或是带来的技术债务，或许就只有项目内的其他成员知道了。</p><p>除此之外，因为答辩是由评委来评分的，因此主观上如果评委比较感兴趣的内容，会更容易通过；而如果是评委熟悉的领域，则会被问到很深入和核心的问题，这样的可能性会更高。</p><p>所以，更多的时候，我认为答辩是否能通过是很需要运气的，包括我自己通过的几次答辩，都有不小的运气成分在里面。这也是为什么，我想跟大家说不要过分依赖晋级答辩，因为如果你过分看重和孤注一掷，那么不管成功与否，都会对你以后的工作心态产生影响。</p><p>那么，正如我视频里所说的，关于答辩这件事，你需要知道：</p><ol><li>答辩是由 70% 的努力 + 30% 的运气组成的。</li><li>答辩考核的除了工作内容，还有工作方式和答辩技巧。</li><li>答辩是结果，不是目的。</li></ol><p>既然我们还是需要认真对待答辩这件事，该怎么去进行准备呢？</p><h2 id="如何准备答辩"><a href="#如何准备答辩" class="headerlink" title="如何准备答辩"></a>如何准备答辩</h2><p>其实，答辩本身也属于项目复盘的一种方式，所以其实我们在平时工作里，就可以用更优的工作方式和节奏，去把事情做好。</p><h3 id="平时工作要做好"><a href="#平时工作要做好" class="headerlink" title="平时工作要做好"></a>平时工作要做好</h3><p>如果我们在平时工作中，就有认真地思考每一个项目，更加结构化地去关注项目中的每个阶段的话，相比答辩本身能给我们自身带来更多的成长。</p><p>我们会常常看到，需要开发在工作的时候基本上是线性的工作方式，即：遇到问题 -&gt; 解决问题 -&gt; 结束。</p><p>实际上，我们可以在每个问题上思考更多：</p><p><strong>(1) 做一件事的目的，需要贯穿全过程。</strong></p><p>很多时候，我们在遇到一个问题的时候，马上就开始找解决方案了。其实我们可以先暂停，去思考下这个问题是如何产生的，我们需要解决的到底是什么程度的问题，做这件事的目的是什么。</p><p>而在问题处理完成之后，同样需要回顾当初这个问题的目的是否已经达成，是否还遗留有待解决的问题，等等。</p><p><strong>(2) 拓展自身的思维，更加结构化地去做事。</strong></p><p>比如，在寻找解决方案的时候，当我们找到一个解决方向的时候，可以先不着急去马上解决，而是需要考虑是否还有其他解决方案？当前方案是否最优？解决方案是否存在局限？是否有更多的探索可能性？</p><p>充分做好前期调研之后，再对多个方案进行对比，结合自身项目的情况，找到最适合用于项目中的一个解决方案。</p><p><strong>(3) 将一件事情的价值最大化。</strong></p><p>很多时候，我们处理完一个问题，这个事情就结束了。对于团队来说，这样的方式其实效率很低，因为不同的团队成员很可能会遇到相同的问题，如果每个人都花费这些时间去获得差不多的结论，那么团队的成长会很慢。</p><p>我们可以选择将每次处理问题的过程和解决方案进行总结沉淀，然后分享给其他人。这样，团队内就可以共享每个人努力的成果，这对于团队来说成长是很快的，而对团队中的每个人来说亦是如此。</p><p>而沉淀和总结本身，也可以促进个人的成长。在开发的职业生涯是，是否具备这样的能力和认识，是十分关键的。</p><h3 id="答辩-项目内容结构"><a href="#答辩-项目内容结构" class="headerlink" title="答辩/项目内容结构"></a>答辩/项目内容结构</h3><p>对于答辩本身，我们首先要知道：要能让评委认可你的能力，首先得高效地让评委理解项目中的各个过程。</p><p>因此，大多数时候我们的答辩内容都可以分为以下结构：</p><ol><li>项目背景/问题描述。讲清楚做这个项目的背景情况和目的，这是最起码的铺垫。</li><li>难点/挑战点。如果评委感受不到项目中的难点，那么这个项目又怎么证明你的能力呢？</li><li>方案调研/方案对比。工作方式中，做好前期足够的调研和准备，认真对比得到的解决方案，才可以说是合适的方案。</li><li>（解决过程）。过程大多数时候无关紧要，但是如果同样存在难点，也可以一并描述。</li><li>项目结果（最终效果/数据论证）。如果有足够的证据佐证，那么这个项目的成果便是无可置疑的。</li><li>展望：遗留问题/后续计划/产生更多价值。从点到面发散这个项目，是否可以做更多？</li><li>个人影响力。</li></ol><p>这里就不过多描述了，其实如果你有认真思考以上的点，基本就可以说是有认真对待一个项目，同时自己也能从中获得足够多的成长和沉淀了。</p><p>除去答辩本身，以上的这些内容其实在我们日常的工作里，同样需要进行思考和去完成的。也就是说，这样的结构点，并不只是答辩所需，而是需要贯彻到我们工作的每个项目/每个遇到的问题里，这样才能更好地脚踏实地，同时也不需要再为答辩专门做更多的处理了。</p><h3 id="答辩技巧"><a href="#答辩技巧" class="headerlink" title="答辩技巧"></a>答辩技巧</h3><p>如果说我们在平时就已经把工作结构化地做好了，是否意味着答辩就能一定顺利呢？</p><p>除了内容本身需要踏实以外，我们还需要掌握一定的答辩技巧，比如:</p><ul><li>PPT 思路清晰，可以参考上述答辩内容结构来进行梳理</li><li>适当使用动画，突出重点。动画的用处在于让对方注意力聚焦在自己讲的内容上，所以要避免过分浮夸的动画</li><li>陈述足够熟练/脱稿，自己或是找同事多练几遍</li><li>思考项目中的不足/可能提问的问题，准备到如何回答</li></ul><p>以上等等。</p><p>如果你有时间，可以来看看这个视频（<a href="https://www.bilibili.com/video/BV1tu411X7qn/" target="_blank" rel="noopener">也可以直接去 B 站看原视频哦</a>）：</p><div style="position: relative; padding: 30% 45%;"><br><br><iframe style="position: absolute; width: 100%; height: 100%; left: 0; top: 0;" src="https://player.bilibili.com/player.html?aid=509223755&bvid=BV1tu411X7qn&cid=512396881&page=1&high_quality=1" frameborder="no" scrolling="no"><br><br></iframe><br><br></div><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>在工作中，我看到过不少由于晋级失败、拿了差考核而开始怀疑自我的小伙伴。我想说的是，工作只是人生的一部分，并不代表着全部，也不可以因为工作的不顺利而否定或是认定自己的一生。</p><p>实际上，失败才是大多数人一生的主旋律，我们要尽早学会如何与失望和意外相处，要接受不完美的自己，学会认可自己。世界上有无数的人，失败或是成功，但是只有一个自己，要学会爱上这个自己。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;最近又是答辩季，程序员最讨厌写的 PPT 又到了不得不写的时候了。之前也有在帮一些小伙伴做准备，所以顺便给大家分享一些晋级答辩的思考和技巧吧～&lt;/p&gt;
    
    </summary>
    
      <category term="工作这杯茶" scheme="https://godbasin.github.io/categories/%E5%B7%A5%E4%BD%9C%E8%BF%99%E6%9D%AF%E8%8C%B6/"/>
    
    
      <category term="心态" scheme="https://godbasin.github.io/tags/%E5%BF%83%E6%80%81/"/>
    
  </entry>
  
  <entry>
    <title>我所理解的前端工程化</title>
    <link href="https://godbasin.github.io/2022/02/07/front-end-engineering/"/>
    <id>https://godbasin.github.io/2022/02/07/front-end-engineering/</id>
    <published>2022-02-07T07:22:23.000Z</published>
    <updated>2022-02-07T07:33:20.409Z</updated>
    
    <content type="html"><![CDATA[<p>前端工程化这个词出现的频率越来越高，一直没有明确的定义，有些人认为是模块化和自动化的工具，比如 Webpack/Gulp、脚手架、组件化等，但工具只是一些辅助手段，并不能作为工程化来理解。</p><a id="more"></a><p>个人认为，前端工程化致力于提升工程的开发效率、协作效率、项目质量，贯穿项目设计、开发、测试、上线、维护的整个过程。</p><p>那么，工程化要解决的问题，便是我们在这些过程中遇到的一些问题了。</p><h1 id="前端项目开发常见问题"><a href="#前端项目开发常见问题" class="headerlink" title="前端项目开发常见问题"></a>前端项目开发常见问题</h1><p>相信大家的日常工作中也能感受到，相比于从 0 到 1 搭建项目，我们的大部分工作都是在维护项目，基于现有的项目上进行迭代和开发。正所谓铁打的营盘流水的兵，每个项目都会经历很多开发的参与、协作和交接，在这个过程中常常会遇到很多的问题，这些问题可以分为两类：<strong>系统质量的下降</strong>，以及<strong>开发效率的下降</strong>。</p><h2 id="系统质量"><a href="#系统质量" class="headerlink" title="系统质量"></a>系统质量</h2><blockquote><p>“没有 BUG 的系统是不存在的。”  – 《被删的开发手册》</p></blockquote><h3 id="系统质量的下降"><a href="#系统质量的下降" class="headerlink" title="系统质量的下降"></a>系统质量的下降</h3><p>BUG 的出现有很多的可能性，比如需求设计不严谨、代码实现的逻辑有漏洞、不在预期之内的异常逻辑分支，等等。除了方案设计和思考的经验不足，BUG 很多时候也会因为对项目的不熟悉、对系统的理解不深入引入，这意味着以下的过程会导致 BUG 的增加：</p><ol><li>项目频繁地调整（新增或者更换）开发人员，由于不熟悉项目，每个新加入的小伙伴都可能会埋下新的 BUG。</li><li>系统功能新增和迭代、不断壮大，各个模块间的耦合增加、复杂度增加。如果没法掌握系统的所有细节，很可能牵一发而动全身，产生自己认知以外的 BUG。</li></ol><p>对于处于快速迭代、不断拓展阶段的项目来说，不管是人员的变动、还是项目的拓展都是无法避免的。除此之外，为了降低系统的复杂度，当项目发展到一定阶段的时候，会对系统进行局部或是整体的架构调整，比如模块的拆分、各个模块间的依赖解耦、引入新的状态管理工具、重复逻辑进行抽象和封装等等。</p><p>新技术的引入会缓解系统复杂度带来的稳定性问题，但同时也可能会引入新的问题，比如：</p><ul><li>部分功能无法与新技术兼容，成为历史遗留问题</li><li>较大范围的架构调整影响面很广，可能埋下难以发现的 BUG</li></ul><p>可见，一个项目不断发展的过程中，都会面临系统质量下降的问题。</p><h3 id="提升系统质量"><a href="#提升系统质量" class="headerlink" title="提升系统质量"></a>提升系统质量</h3><p>为了提升系统质量，我们需要对项目进行合理的架构调整，提升系统的可读性、可维护性、可测试行、稳定性，从而提升系统发布的稳定性。</p><p>我们在进行架构设计时，需要根据项目的预期和现状来设计，保留拓展性的同时，避免过度设计。因此，随着项目不断发展，原有的架构设计可能不再适合，此时我们需要进行优化和调整，比如：</p><ul><li>引入新的技术和工具</li><li>团队成员增加，沟通成本和对规范的理解出现差异</li><li>项目代码量和文件数的增加</li><li>进行自动化测试能力的覆盖</li><li>搭建完善的监控和告警体系</li></ul><p>在这个过程中，我们可能分别引入了新的代码构建、代码规范和自动化测试工具，搭建了新的监控系统、发布系统、流程控制等，这些都属于前端工程化的一部分。</p><p>但是对于开发来说，开发流程变得繁琐，意味着工作内容更复杂，同时还增加了很多新工具和系统的熟悉成本。那么，我们还可以通过优化项目的研发和发布流程，来提升项目的开发效率。</p><h2 id="开发效率"><a href="#开发效率" class="headerlink" title="开发效率"></a>开发效率</h2><blockquote><p>“今天又要加班了，因为今天的代码还没开始写。” – 《被删的开发手册》</p></blockquote><h3 id="开发效率的下降"><a href="#开发效率的下降" class="headerlink" title="开发效率的下降"></a>开发效率的下降</h3><p>系统上线之后，开发的工作内容重心，会从功能开发逐渐转向其它内容。除了新功能的评审和设计以外，还会包括：</p><ul><li>用户反馈问题跟进和定位</li><li>线上 BUG 修复和紧急发布</li><li>处理系统的监控告警，排查异常问题</li><li>新功能灰度发布过程，自测、产品验证功能、提测、修复 BUG、灰度发布等各个流程都需要人工操作和主动关注</li><li>为了保证系统质量，需要完善自动化测试能力，包括单元测试、UI 测试、集成测试等</li><li>项目成员的调整，需要进行工作的交接、指导对方的工作内容等</li></ul><p>开发的工作内容变得复杂，需要关注的事情也更多，对于各个系统（监控告警系统、日志系统、测试系统、发布系统等）也都需要熟悉成本和操作成本。在各个工作内容之间切换，也常常容易出现步骤的遗漏，导致一些流程上的问题，比如：</p><ol><li>系统灰度到一半，处理其它事情忘了全量。</li><li>系统发布之后，去处理紧急 BUG、忘记看监控，直到收到大量的用户反馈。</li><li>线上紧急 BUG 修复了，急着发布忘了进行自动化测试。</li></ol><p>随着项目规模变大，系统的复杂度也随之上升，上面所提到的工作量也都会增加，开发效率会肉眼可见地受到影响。以前一天工作量的功能开发，如今需要三天时间才能完成，因为每天只有三分之一的时间（甚至更少）可以用来开发新功能。</p><p>在这个项目阶段，开发每天的杂事太多、效率太低、浑浑噩噩不知道都做了些什么，团队面临着项目复杂度上升、系统质量不稳定、技术债务越来越多、团队工作效率下降等问题。</p><h3 id="提升开发效率"><a href="#提升开发效率" class="headerlink" title="提升开发效率"></a>提升开发效率</h3><p>项目研发和发布流程优化的核心点在于：将一切需要手动操作和关注的内容自动化。</p><p>那么，我们先来梳理下项目开发和发布过程中，到底有多少繁琐的工作可以进行自动化。一般来说，开发在接到产品需求单后，会涉及到分支管理、代码构建、环境部署、测试/验证、问题修复、灰度发布、监控告警、需求单状态扭转等各个流程。</p><p>每一次功能发布，都需要花费很多的精力在各个流程步骤上。我们可以将这些步骤都转为自动化，就可以让开发的精力聚焦在功能的设计和实现上。对于流程自动化，业界比较成熟的解决方案是使用持续集成（continuous integration，简称 CI）和持续部署（continuous deployment，简称 CD）：</p><ul><li>持续集成（CI）：目的是让产品可以快速迭代，同时还能保持高质量</li><li>持续部署（CD）：目的是代码在任何时刻都是可部署、可进入生产阶段</li></ul><p>CI 在项目中的实践，指团队成员频繁（一天多次）地提交代码到主干分支，每次提交后会自动触发自动化验证的任务集合，以便尽可能地提前发现问题；CD 在项目中的实践，指代码通过评审以后，可自动化、可控、多批次地部署到生产环境，CD 的前提是能自动化完成测试、构建、部署等步骤。</p><p>CI/CD 在项目中的落地，很多时候会表现为流水线的开发模式：通过建立完整的 CI/CD 流水线，涵盖整个研发流程，可有效地提高效率。一般来说，我们可以搭建这样的 CI/CD 流水线：</p><ul><li>需求单分配：分配并自动拉取相应 Git 分支</li><li>代码提交：代码规范检查 + 自动化测试 + 部署测试环境 + 根据需求单配置通知相应的产品和测试</li><li>产品验证/功能测试：BUG 单自动关联需求单和分支 + 验证完成后，根据需求单通知开发侧</li><li>BUG 修复：代码规范检查 + 自动化测试 + 部署测试环境 + 根据分支 BUG 单和需求单通知测试 + 验证完成后，根据需求单通知开发侧</li><li>代码合入主干：向团队成员发起代码 Review + Review 通过后代码合入主干</li><li>日常发布：定时器发起发布流程 + 预发布环境部署 + 进行自动化测试 + 测试通过后进入灰度过程</li><li>灰度发布：根据配置比例进行灰度 + 灰度过程中自动化进行监控 + 可选择性进入快速回滚流程</li><li>全量发布：自动扭转需求单状态，并将版本进行归档（Git Tag）</li></ul><p>通过将以上流程自动化，可以节省开发的很多人工操作时间、提升开发效率的同时，也避免了人工操作容易出现的遗漏和失误。将自动化流水线与通知/告警机器人、工作群、需求单系统、BUG 系统、代码管理系统、发布系统、监控系统结合，实现全研发和发布流程的自动化，开发可从各种杂事中释放，专注于功能开发的实现。</p><p>越是大规模、系统建设完备的团队，开发流程中消耗在多人协作和各个系统的操作中的精力越多，搭建 CI/CD 后更能体会到自动化流程带来的便利。</p><p>当然，搭建 CI/CD 的过程中，也需要投入不少的人力精力。因此，很多时候我们可以考虑性价比，从对研发效能影响最大的痛点开始进行建设，可以最快速和有效地提升团队的开发效率，让更多的人愿意参与到 CI/CD 的建设中。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><blockquote><p>“能用机器解决的问题，就不要依赖人。” – 《被删的开发手册》</p></blockquote><p>项目维护阶段的最大痛点，其实在于开发无法聚焦自身的工作内容，常常需要在各种系统中进行操作和切换，从而带来开发效率的下降，以及注意力分散、无法更全面的思考导致了不合理的设计、新的 BUG 引入，而影响了系统的质量。</p><p>前端工程化的出现，正是为了解决系统质量和效率低下的问题。但前端工程化并不只是局限于代码构建和流水线，可以将其理解为解决项目开发过程中遇到的所有问题，目的在于提升系统质量和开发效率。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;前端工程化这个词出现的频率越来越高，一直没有明确的定义，有些人认为是模块化和自动化的工具，比如 Webpack/Gulp、脚手架、组件化等，但工具只是一些辅助手段，并不能作为工程化来理解。&lt;/p&gt;
    
    </summary>
    
      <category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
    
    
      <category term="逻辑实现" scheme="https://godbasin.github.io/tags/%E9%80%BB%E8%BE%91%E5%AE%9E%E7%8E%B0/"/>
    
  </entry>
  
  <entry>
    <title>Angular框架解读--Ivy编译器之变更检测</title>
    <link href="https://godbasin.github.io/2022/01/09/angular-design-ivy-6-detect-change/"/>
    <id>https://godbasin.github.io/2022/01/09/angular-design-ivy-6-detect-change/</id>
    <published>2022-01-09T12:01:48.000Z</published>
    <updated>2022-01-09T12:10:04.321Z</updated>
    
    <content type="html"><![CDATA[<p>作为“为大型前端项目”而设计的前端框架，Angular 其实有许多值得参考和学习的设计，本系列主要用于研究这些设计和功能的实现原理。本文围绕 Angular 的核心功能 Ivy 编译器，介绍其中变更检测的过程。</p><a id="more"></a><p>上一篇<a href="https://godbasin.github.io/2021/12/05/angular-design-ivy-5-incremental-dom/">《Angular框架解读–Ivy编译器之增量DOM》</a>中，我介绍了 Ivy 编译器中使用了增量 DOM 的设计。在 Ivy 中，通过编译器将模板编译为<code>template</code>渲染函数，该过程会将对模板的解析编译成增量 DOM 相关的指令。其中，在<code>elementStart()</code>执行时，我们可以看到会通过<code>createElementNode()</code>方法来创建 DOM。</p><p>而增量 DOM 中的变更检测、Diff 和更新 DOM 等能力，都与<code>elementStart()</code>方法紧紧关联着。</p><h2 id="Ivy-中的变更检测"><a href="#Ivy-中的变更检测" class="headerlink" title="Ivy 中的变更检测"></a>Ivy 中的变更检测</h2><h3 id="ngZone-的自动变更检测"><a href="#ngZone-的自动变更检测" class="headerlink" title="ngZone 的自动变更检测"></a>ngZone 的自动变更检测</h3><p>在<a href="https://godbasin.github.io/2021/05/30/angular-design-zone-ngzone/">《Angular框架解读–Zone区域之ngZone》</a>一文中，我们介绍了默认情况下，所有异步操作都在 Angular Zone 内。该逻辑在创建 Angular 应用的时候便已添加，这会自动触发变更检测：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Injectable</span>()</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> ApplicationRef &#123;</span><br><span class="line">  ...</span><br><span class="line">  <span class="keyword">constructor</span>(<span class="params"></span></span><br><span class="line"><span class="params">      <span class="keyword">private</span> _zone: NgZone, <span class="keyword">private</span> _injector: Injector, <span class="keyword">private</span> _exceptionHandler: ErrorHandler,</span></span><br><span class="line"><span class="params">      <span class="keyword">private</span> _componentFactoryResolver: ComponentFactoryResolver,</span></span><br><span class="line"><span class="params">      <span class="keyword">private</span> _initStatus: ApplicationInitStatus</span>) &#123;</span><br><span class="line">    <span class="comment">// Microtask 为空时，触发变更检测</span></span><br><span class="line">    <span class="keyword">this</span>._onMicrotaskEmptySubscription = <span class="keyword">this</span>._zone.onMicrotaskEmpty.subscribe(&#123;</span><br><span class="line">      next: <span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">this</span>._zone.run(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">          <span class="comment">// tick 为变更检测的逻辑，会重新进行 template 的计算和渲染</span></span><br><span class="line">          <span class="keyword">this</span>.tick();</span><br><span class="line">        &#125;);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>tick</code>方法中，核心的逻辑是调用了<code>view.detectChanges()</code>来检测更新。该接口来自<code>ChangeDetectorRef</code>，它提供变更检测功能的基类。</p><p>变更检测树收集所有要检查变更的视图，可以使用方法从树中添加和删除视图，启动更改检测，并将视图显式标记为<code>_dirty_</code>，这意味着它们已更改并需要重新渲染。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">abstract</span> <span class="keyword">class</span> ChangeDetectorRef &#123;</span><br><span class="line">  <span class="comment">// 当视图中的输入更改或事件触发时，组件通常被标记为脏（需要重新渲染）</span></span><br><span class="line">  <span class="comment">// 调用此方法以确保即使未发生这些触发器也会检查组件</span></span><br><span class="line">  <span class="keyword">abstract</span> markForCheck(): <span class="built_in">void</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 从变更检测树中分离此视图，在重新附加之前不会检查分离的视图</span></span><br><span class="line">  <span class="comment">// 与 detectChanges() 结合使用以实现本地更改检测检查</span></span><br><span class="line">  <span class="keyword">abstract</span> detach(): <span class="built_in">void</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 检查此视图及其子视图</span></span><br><span class="line">  <span class="comment">// 与 detach() 结合使用以实现本地更改检测检查</span></span><br><span class="line">  <span class="keyword">abstract</span> detectChanges(): <span class="built_in">void</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 检查更改检测器及其子项，如果检测到任何更改则抛出</span></span><br><span class="line">  <span class="keyword">abstract</span> checkNoChanges(): <span class="built_in">void</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 将先前分离的视图重新附加到更改检测树</span></span><br><span class="line">  <span class="comment">// 默认情况下，视图附加到树</span></span><br><span class="line">  <span class="keyword">abstract</span> reattach(): <span class="built_in">void</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在上述的<code>ChangeDetectorRef</code>中，变更检测<code>detectChanges()</code>中，核心逻辑调用了<code>refreshView()</code>。</p><h3 id="refreshView-视图更新处理"><a href="#refreshView-视图更新处理" class="headerlink" title="refreshView 视图更新处理"></a>refreshView 视图更新处理</h3><p><code>refreshView()</code>用于在更新模式下处理视图：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">refreshView</span>&lt;<span class="title">T</span>&gt;(<span class="params"></span></span></span><br><span class="line"><span class="function"><span class="params">    tView: TView, lView: LView, templateFn: ComponentTemplate&lt;&#123;&#125;&gt;|<span class="literal">null</span>, context: T</span>) </span>&#123;</span><br><span class="line">  ngDevMode &amp;&amp; assertEqual(isCreationMode(lView), <span class="literal">false</span>, <span class="string">'Should be run in update mode'</span>);</span><br><span class="line">  <span class="keyword">const</span> flags = lView[FLAGS];</span><br><span class="line">  enterView(lView);</span><br><span class="line">  <span class="keyword">const</span> isInCheckNoChangesPass = isInCheckNoChangesMode();</span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    resetPreOrderHookFlags(lView);</span><br><span class="line"></span><br><span class="line">    setBindingIndex(tView.bindingStartIndex);</span><br><span class="line">    <span class="keyword">if</span> (templateFn !== <span class="literal">null</span>) &#123;</span><br><span class="line">      <span class="comment">// 1. 执行 template 模板函数</span></span><br><span class="line">      executeTemplate(tView, lView, templateFn, RenderFlags.Update, context);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 执行预处理钩子，包括 OnInit、OnChanges、DoCheck</span></span><br><span class="line">    <span class="keyword">if</span> (!isInCheckNoChangesPass) &#123;</span><br><span class="line">      <span class="keyword">if</span> (hooksInitPhaseCompleted) &#123;</span><br><span class="line">        executeCheckHooks(lView, preOrderCheckHooks, <span class="literal">null</span>);</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        executeInitAndCheckHooks(lView, preOrderHooks, InitPhaseState.OnInitHooksToBeRun, <span class="literal">null</span>);</span><br><span class="line">        incrementInitPhaseFlags(lView, InitPhaseState.OnInitHooksToBeRun);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 首先将在此 lView 中声明的移植视图标记为需要在其插入点刷新</span></span><br><span class="line">    <span class="comment">// 这是为了避免模板在这个 LView 中定义但它的声明出现在插入组件之后的情况</span></span><br><span class="line">    markTransplantedViewsForRefresh(lView);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 遍历嵌入式视图（通过 ViewContainerRef API 创建的视图）并通过执行关联的模板函数刷新它们</span></span><br><span class="line">    refreshEmbeddedViews(lView);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3. 在调用内容钩子之前，必须刷新内容查询结果</span></span><br><span class="line">    <span class="keyword">if</span> (tView.contentQueries !== <span class="literal">null</span>) &#123;</span><br><span class="line">      refreshContentQueries(tView, lView);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 执行内容钩子，包括 AfterContentInit, AfterContentChecked</span></span><br><span class="line">    <span class="keyword">if</span> (!isInCheckNoChangesPass) &#123;</span><br><span class="line">      <span class="keyword">if</span> (hooksInitPhaseCompleted) &#123;</span><br><span class="line">        executeCheckHooks(lView, contentCheckHooks);</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        executeInitAndCheckHooks(lView, contentHooks, InitPhaseState.AfterContentInitHooksToBeRun);</span><br><span class="line">        incrementInitPhaseFlags(lView, InitPhaseState.AfterContentInitHooksToBeRun);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 4. 设置 host 绑定</span></span><br><span class="line">    processHostBindingOpCodes(tView, lView);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 5. 刷新子组件视图</span></span><br><span class="line">    <span class="keyword">const</span> components = tView.components;</span><br><span class="line">    <span class="keyword">if</span> (components !== <span class="literal">null</span>) &#123;</span><br><span class="line">      refreshChildComponents(lView, components);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 刷新子组件后必须执行视图查询，因为此视图中的模板可以插入子组件中</span></span><br><span class="line">    <span class="comment">// 如果视图查询在子组件刷新之前执行，则模板可能尚未插入</span></span><br><span class="line">    <span class="keyword">const</span> viewQuery = tView.viewQuery;</span><br><span class="line">    <span class="keyword">if</span> (viewQuery !== <span class="literal">null</span>) &#123;</span><br><span class="line">      executeViewQueryFn(RenderFlags.Update, viewQuery, context);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 执行视图钩子，包括 AfterViewInit, AfterViewChecked</span></span><br><span class="line">    <span class="keyword">if</span> (!isInCheckNoChangesPass) &#123;</span><br><span class="line">      <span class="keyword">if</span> (hooksInitPhaseCompleted) &#123;</span><br><span class="line">        executeCheckHooks(lView, viewCheckHooks);</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        executeInitAndCheckHooks(lView, viewHooks, InitPhaseState.AfterViewInitHooksToBeRun);</span><br><span class="line">        incrementInitPhaseFlags(lView, InitPhaseState.AfterViewInitHooksToBeRun);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 我们需要确保我们只在成功的 refreshView 上翻转标志</span></span><br><span class="line">    <span class="keyword">if</span> (tView.firstUpdatePass === <span class="literal">true</span>) &#123;</span><br><span class="line">      tView.firstUpdatePass = <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 在检查无变化模式下运行时不要重置脏状态</span></span><br><span class="line">    <span class="comment">// 例如：在 ngAfterViewInit 钩子中将 OnPush 组件标记为脏组件以刷新 NgClass 绑定应该可以工作</span></span><br><span class="line">    <span class="keyword">if</span> (!isInCheckNoChangesPass) &#123;</span><br><span class="line">      lView[FLAGS] &amp;= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (lView[FLAGS] &amp; LViewFlags.RefreshTransplantedView) &#123;</span><br><span class="line">      lView[FLAGS] &amp;= ~LViewFlags.RefreshTransplantedView;</span><br><span class="line">      updateTransplantedViewCount(lView[PARENT] <span class="keyword">as</span> LContainer, <span class="number">-1</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    leaveView();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到，<code>refreshView()</code>的处理包括按特定顺序执行的多个步骤：</p><ol><li>在更新模式下，执行<code>template</code>模板函数。</li><li>执行钩子。</li><li>刷新 Query 查询。</li><li>设置 host 绑定。</li><li>刷新子（嵌入式和组件）视图。</li></ol><p>除此之外，在变更检测的最开始执行了<code>enterView()</code>，此时 Angular 会用新的<code>LView</code>交换当前的<code>LView</code>。这样的处理主要出于性能原因，通过将<code>LView</code>存储在模块的顶层，最大限度地减少了要读取的属性数量。</p><p><code>LView</code>用于存储从模板调用指令时处理指令所需的所有信息，在<a href>《Angular框架解读–Ivy编译器的视图数据和依赖解析》</a>中有介绍。</p><p>每个嵌入视图和组件视图都有自己的<code>LView</code>。在处理特定视图时，我们将<code>viewData</code>设置为该<code>LView</code>。当该视图完成处理后，<code>viewData</code>被设置回原始<code>viewData</code>之前的任何内容（父<code>LView</code>）。</p><p>在<code>refreshView()</code>处理中，每当进入新视图时会存储<code>LView</code>以备后用。我们也可以看到当退出视图时，通过执行<code>leaveView()</code>离开当前的<code>LView</code>，恢复原来的状态。</p><p>以上便是变更检测过程中的视图处理逻辑。</p><h3 id="创建与更新视图的处理"><a href="#创建与更新视图的处理" class="headerlink" title="创建与更新视图的处理"></a>创建与更新视图的处理</h3><p>我们可以对比下创建视图的过程，处理视图创建的过程在<code>renderView()</code>中实现。</p><p><code>renderView()</code>用于在创建模式下处理视图，该过程包括按特定顺序执行的多个步骤：</p><ol><li>创建视图查询函数（如果有）。</li><li>在创建模式下，执行<code>template()</code>模板函数。</li><li>更新静态 Query 查询（如果有）。</li><li>创建在给定视图中定义的子组件。</li></ol><p>在上一篇文章中，我们介绍了这样一个组件：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; Component, Input &#125; <span class="keyword">from</span> <span class="string">"@angular/core"</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Component</span>(&#123;</span><br><span class="line">  selector: <span class="string">"greet"</span>,</span><br><span class="line">  template: <span class="string">"&lt;div&gt; Hello, &#123;&#123;name&#125;&#125;! &lt;/div&gt;"</span>,</span><br><span class="line">&#125;)</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> GreetComponent &#123;</span><br><span class="line">  <span class="meta">@Input</span>() name: <span class="built_in">string</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>经<code>ngtsc</code>编译后，产物会大概长这个样子：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">GreetComponent.ɵcmp = i0.ɵɵdefineComponent(&#123;</span><br><span class="line">  type: GreetComponent,</span><br><span class="line">  tag: <span class="string">"greet"</span>,</span><br><span class="line">  factory: <span class="function"><span class="params">()</span> =&gt;</span> <span class="keyword">new</span> GreetComponent(),</span><br><span class="line">  template: <span class="function"><span class="keyword">function</span> (<span class="params">rf, ctx</span>) </span>&#123;</span><br><span class="line">    <span class="comment">// 创建模式下</span></span><br><span class="line">    <span class="keyword">if</span> (rf &amp; RenderFlags.Create) &#123;</span><br><span class="line">      i0.ɵɵelementStart(<span class="number">0</span>, <span class="string">"div"</span>);</span><br><span class="line">      i0.ɵɵtext(<span class="number">1</span>);</span><br><span class="line">      i0.ɵɵelementEnd();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 更新模式下</span></span><br><span class="line">    <span class="keyword">if</span> (rf &amp; RenderFlags.Update) &#123;</span><br><span class="line">      i0.ɵɵadvance(<span class="number">1</span>);</span><br><span class="line">      i0.ɵɵtextInterpolate1(<span class="string">"Hello "</span>, ctx.name, <span class="string">"!"</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>可以看到，创建模式下的模板函数逻辑，与更新视图模式下的模板函数逻辑是有区别的。在创建模式下，<code>elementStart</code>、<code>elementEnd</code>我们在上一篇文章中有详细地介绍了。而在更新模式下，<code>textInterpolate1</code>表示当文本节点有 1 个内插值时，使用由其他文本包围的单个绑定值更新文本内容：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">interpolation1</span>(<span class="params">lView: LView, prefix: <span class="built_in">string</span>, v0: <span class="built_in">any</span>, suffix: <span class="built_in">string</span></span>): <span class="title">string</span>|</span></span><br><span class="line"><span class="function">    <span class="title">NO_CHANGE</span> </span>&#123;</span><br><span class="line">  <span class="keyword">const</span> different = bindingUpdated(lView, nextBindingIndex(), v0);</span><br><span class="line">  <span class="keyword">return</span> different ? prefix + renderStringify(v0) + suffix : NO_CHANGE;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以见到，在具体的模板函数指令中，会自行进行变更的检查，如果有发生了变化，则进行更新。<code>bindingUpdated()</code>方法会在需要更改时更新绑定，然后返回是否已更新。</p><p>而对于视图更新时，除了<code>textInterpolate1</code>这种比较简单的场景下的模板更新，子组件通过<code>refreshComponent</code>来处理：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">refreshComponent</span>(<span class="params">hostLView: LView, componentHostIdx: <span class="built_in">number</span></span>): <span class="title">void</span> </span>&#123;</span><br><span class="line">  ngDevMode &amp;&amp; assertEqual(isCreationMode(hostLView), <span class="literal">false</span>, <span class="string">'Should be run in update mode'</span>);</span><br><span class="line">  <span class="keyword">const</span> componentView = getComponentLViewByIndex(componentHostIdx, hostLView);</span><br><span class="line">  <span class="comment">// 仅应刷新 CheckAlways 或 OnPush 且 Dirty 的附加组件</span></span><br><span class="line">  <span class="keyword">if</span> (viewAttachedToChangeDetector(componentView)) &#123;</span><br><span class="line">    <span class="keyword">const</span> tView = componentView[TVIEW];</span><br><span class="line">    <span class="keyword">if</span> (componentView[FLAGS] &amp; (LViewFlags.CheckAlways | LViewFlags.Dirty)) &#123;</span><br><span class="line">      <span class="comment">// 此处检测组件是否被标记为 CheckAlways 或者 Dirty，此时才进行该组件的视图更新</span></span><br><span class="line">      refreshView(tView, componentView, tView.template, componentView[CONTEXT]);</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (componentView[TRANSPLANTED_VIEWS_TO_REFRESH] &gt; <span class="number">0</span>) &#123;</span><br><span class="line">      <span class="comment">// 仅应刷新 CheckAlways 或 OnPush 且脏的附加组件</span></span><br><span class="line">      refreshContainsDirtyView(componentView);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>同样的，在处理子组件的时候，需要检查子组件是否被标记为 CheckAlways 或者 Dirty，才进入组件视图并处理其绑定、查询等来刷新组件。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>以上，便是 Angular Ivy 中的变更检测了。</p><p>可以看到，在 Angular 中将被标记为 CheckAlways 或者 Dirty 的组件进行视图刷新，在每个变更周期中，会执行<code>template()</code>模板函数中的更新模式下逻辑。而在<code>template()</code>模板函数中的具体指令逻辑中，还会根据原来的值和新的值进行比较，有差异的时候才会进行更新。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://indepth.dev/posts/1271/angular-ivy-change-detection-execution-are-you-prepared" target="_blank" rel="noopener">Angular Ivy change detection execution: are you prepared?</a></li><li><a href="https://indepth.dev/posts/1062/ivy-engine-in-angular-first-in-depth-look-at-compilation-runtime-and-change-detection" target="_blank" rel="noopener">Ivy engine in Angular: first in-depth look at compilation, runtime and change detection</a></li><li><a href="https://indepth.dev/posts/1053/everything-you-need-to-know-about-change-detection-in-angular" target="_blank" rel="noopener">Everything you need to know about change detection in Angular</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;作为“为大型前端项目”而设计的前端框架，Angular 其实有许多值得参考和学习的设计，本系列主要用于研究这些设计和功能的实现原理。本文围绕 Angular 的核心功能 Ivy 编译器，介绍其中变更检测的过程。&lt;/p&gt;
    
    </summary>
    
      <category term="Angular源码" scheme="https://godbasin.github.io/categories/Angular%E6%BA%90%E7%A0%81/"/>
    
    
      <category term="功能设计" scheme="https://godbasin.github.io/tags/%E5%8A%9F%E8%83%BD%E8%AE%BE%E8%AE%A1/"/>
    
  </entry>
  
</feed>
