浏览器工作原理和实践(三)——页面 (上)

简介:   《浏览器工作原理与实践》是极客时间上的一个浏览器学习系列,在学习之后特在此做记录和总结。

一、事件循环


  消息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点,也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。

40.png

  从上图可以看出,改造可以分为下面三个步骤:

  (1)添加一个消息队列;

  (2)IO 线程中产生的新任务添加进消息队列尾部;

  (3)渲染主线程会循环地从消息队列头部中读取任务,执行任务。

1)处理其他进程发送过来的任务

  从图中可以看出,渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程,后续的步骤就和前面讲解的“处理其他线程发送的任务”一样了。


41.png


2)消息队列中的任务类型

  包含很多内部消息类型,如输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JavaScript 定时器等。

  除此之外,消息队列中还包含了很多与页面相关的事件,如 JavaScript 执行、解析 DOM、样式计算、布局计算、CSS 动画等。

  以上这些事件都是在主线程中执行的,所以在编写 Web 应用时,还需要衡量这些事件所占用的时长,并想办法解决单个任务占用主线程过久的问题。

3)安全退出

  确定要退出当前页面时,页面主线程会设置一个退出标志的变量,在每次执行完一个任务时,判断是否有设置退出标志。

  如果设置了,那么就直接中断当前的所有任务,退出线程。

4)单线程的缺点

  (1)第一个问题是如何处理高优先级的任务。

  如果 DOM 发生变化,采用同步通知的方式,会影响当前任务的执行效率;如果采用异步方式,又会影响到监控的实时性。

  针对这种情况,微任务就应运而生了,下面来看看微任务是如何权衡效率和实时性的。

  通常把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列,在执行宏任务的过程中,如果 DOM 有变化,那么就会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行,因此也就解决了执行效率的问题。

  等宏任务中的主要功能都执行完成之后,这时候,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,因为 DOM 变化的事件都保存在这些微任务队列中,这样也就解决了实时性问题。

  (2)第二个是如何解决单个任务执行时长过久的问题。

  针对这种情况,JavaScript 可以通过回调功能来规避这种问题,也就是让要执行的 JavaScript 任务滞后执行。


二、WebAPI


1)定时器

  在 Chrome 中除了正常使用的消息队列之外,还有另外一个消息队列,这个队列中维护了需要延迟执行的任务列表,包括了定时器和 Chromium 内部一些需要延迟执行的任务。

  所以当通过 JavaScript 创建一个定时器时,渲染进程会将该定时器的回调任务添加到延迟队列中。

  当通过 JavaScript 调用 setTimeout 设置回调函数的时候,渲染进程将会创建一个回调任务,包含了回调函数 showName、当前发起时间和延迟执行时间。

  处理完消息队列中的一个任务之后,就开始执行延迟函数。该函数会根据发起时间和延迟时间计算出到期的任务,然后依次执行这些到期的任务。等到期的任务执行完成之后,再继续下一个循环过程。通过这样的方式,一个完整的定时器就实现了。

  使用定时器的注意事项:

  (1)如果当前任务执行时间过久,会影响定时器任务的执行。

  (2)如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒。

  (3)未激活的页面,setTimeout 执行最小间隔是 1000 毫秒。

  (4)延时执行时间有最大值。

  (5)使用 setTimeout 设置的回调函数中的 this 不符合直觉,方法中的 this 关键字将指向全局环境。

2)XMLHttpRequest

  XMLHttpRequest的工作过程可以参考下图:


42.png


  setTimeout 是直接将延迟任务添加到延迟队列中,而 XMLHttpRequest 发起请求,是由浏览器的其他进程或者线程去执行,然后再将执行结果利用 IPC 的方式通知渲染进程,之后渲染进程再将对应的消息添加到消息队列中。

3)requestAnimationFrame

  根据实际情况,动态调整消息队列的优先级。


43.png


  这张图展示了 Chromium 在不同的场景下,是如何调整消息队列优先级的。通过这种动态调度策略,就可以满足不同场景的核心诉求了,同时这也是 Chromium 当前所采用的任务调度策略。

  当显示器将一帧画面绘制完成后,并在准备读取下一帧之前,显示器会发出一个垂直同步信号(Vertical Synchronization)给 GPU,简称 VSync。

  具体地讲,当 GPU 接收到 VSync 信号后,会将 VSync 信号同步给浏览器进程,浏览器进程再将其同步到对应的渲染进程,渲染进程接收到 VSync 信号之后,就可以准备绘制新的一帧了,具体流程你可以参考下图:


44.png


  在合成完成之后,合成线程会提交给渲染主线程提交完成合成的消息,如果当前合成操作执行的非常快,比如从用户发出消息到完成合成操作只花了 8 毫秒,因为 VSync 同步周期是 16.66(1/60)毫秒,那么这个 VSync 时钟周期内就不需要再次生成新的页面了。那么从合成结束到下个 VSync 周期内,就进入了一个空闲时间阶段,那么就可以在这段空闲时间内执行一些不那么紧急的任务,比如 V8 的垃圾回收,或者通过 window.requestIdleCallback() 设置的回调任务等,都会在这段空闲时间内执行。

  CSS 动画是由渲染进程自动处理的,所以渲染进程会让 CSS 渲染每帧动画的过程与 VSync 的时钟保持一致, 这样就能保证 CSS 动画的高效率执行。

  但是 JavaScript 是由用户控制的,如果采用 setTimeout 来触发动画每帧的绘制,那么其绘制时机是很难和 VSync 时钟保持一致的,所以 JavaScript 中又引入了 window.requestAnimationFrame,用来和 VSync 的时钟周期同步,它的回调任务会在每一帧的开始执行。


三、宏任务和微任务


1)宏任务

  页面中的大部分任务都是在主线程上执行的,这些任务包括了:

  (1)渲染事件(如解析 DOM、计算布局、绘制);

  (2)用户交互事件(如鼠标点击、滚动页面、放大缩小等);

  (3)JavaScript 脚本执行事件;

  (4)网络请求完成、文件读写完成事件。

  为了协调这些任务有条不紊地在主线程上执行,页面进程引入了消息队列和事件循环机制,渲染进程内部会维护多个消息队列,比如延迟执行队列和普通的消息队列。然后主线程采用一个 for 循环,不断地从这些任务队列中取出任务并执行任务。把这些消息队列中的任务称为宏任务。

  页面的渲染事件、各种 IO 的完成事件、执行 JavaScript 脚本的事件、用户交互的事件等都随时有可能被添加到消息队列中,而且添加事件是由系统操作的,JavaScript 代码不能准确掌控任务要添加到队列中的位置,控制不了任务在消息队列中的位置,所以很难控制开始执行任务的时间。


<!DOCTYPE html>
<html>
    <body>
        <div id='demo'>
            <ol>
                <li>test</li>
            </ol>
        </div>
    </body>
    <script type="text/javascript">
        function timerCallback2(){
          console.log(2)
        }
        function timerCallback(){
            console.log(1)
            setTimeout(timerCallback2,0)
        }
        setTimeout(timerCallback,0)
    </script>
</html>


  在这段代码中,目的是想通过 setTimeout 来设置两个回调任务,并让它们按照前后顺序来执行,中间也不要再插入其他的任务。

  但实际情况是不能控制的,比如在调用 setTimeout 来设置回调任务的间隙,消息队列中就有可能被插入很多系统级的任务。


45.png


  所以说宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合了,比如监听 DOM 变化的需求。

2)微任务

  微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。也就是说每个宏任务都关联了一个微任务队列。

  当 JavaScript 执行一段脚本的时候,V8 会为其创建一个全局执行上下文,在创建全局执行上下文的同时,V8 引擎也会在内部创建一个微任务队列。

  在现代浏览器里面,产生微任务有两种方式。

  (1)第一种方式是使用 MutationObserver 监控某个 DOM 节点,然后再通过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。

  (2)第二种方式是使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务。

  通常情况下,在当前宏任务中的 JavaScript 快执行完成时,也就在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。

  WHATWG 把执行微任务的时间点称为检查点。

  在执行微任务过程中产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行。


46.png


  在 JavaScript 脚本的后续执行过程中,分别通过 Promise 和 removeChild 创建了两个微任务,并被添加到微任务列表中。接着 JavaScript 执行结束,准备退出全局执行上下文,这时候就到了检查点了,JavaScript 引擎会检查微任务列表,发现微任务列表中有微任务,那么接下来,依次执行这两个微任务。等微任务队列清空之后,就退出全局执行上下文。

  从上面分析可以得出如下几个结论:

  (1)微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。

  (2)微任务的执行时长会影响到当前宏任务的时长。

  (3)在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。

3)监听 DOM 变化方法

  MutationObserver 是用来监听 DOM 变化的一套方法。MutationObserver API 可以用来监视 DOM 的变化,包括属性的变化、节点的增减、内容的变化等。

  相比较 Mutation Event,MutationObserver 的改进如下:

  (1)首先,MutationObserver 将响应函数改成异步调用,可以不用在每次 DOM 变化都触发异步调用,而是等多次 DOM 变化后,一次触发异步调用,并且还会使用一个数据结构来记录这期间所有的 DOM 变化。

  (2)在每次 DOM 节点发生变化的时候,渲染引擎将变化记录封装成微任务,并将微任务添加进当前的微任务队列中。这样当执行到检查点的时候,V8 引擎就会按照顺序执行微任务了。


四、Promise


  如果你想要学习一门新技术,最好的方式是先了解这门技术是如何诞生的,以及它所解决的问题是什么。了解了这些后,你才能抓住这门技术的本质。

  如果嵌套了太多的回调函数就很容易使得自己陷入了回调地狱,并且代码看起来会很乱,原因如下。

  (1)第一是嵌套调用,下面的任务依赖上个任务的请求结果,并在上个任务的回调函数内部执行新的业务逻辑,这样当嵌套层次多了之后,代码的可读性就变得非常差了。

  (2)第二是任务的不确定性,执行每个任务都有两种可能的结果(成功或者失败),这种对每个任务都要进行一次额外的错误处理的方式,明显增加了代码的混乱程度。

  原因分析出来后,那么问题的解决思路就很清晰了:

  (1)第一是消灭嵌套调用;

  (2)第二是合并多个任务的错误处理。

1)消灭嵌套

  Promise 主要通过下面两步解决嵌套回调问题的。

  (1)首先,Promise 实现了回调函数的延时绑定。

  回调函数的延时绑定在代码上体现就是先创建 Promise 对象 x1,通过 Promise 的构造函数 executor 来执行业务逻辑;创建好 Promise 对象 x1 之后,再使用 x1.then 来设置回调函数。

  (2)其次,需要将回调函数 onResolve 的返回值穿透到最外层。

  因为根据 onResolve 函数的传入值来决定创建什么类型的 Promise 任务,创建好的 Promise 对象需要返回到最外层,这样就可以摆脱嵌套循环了。


47.png


2)合并错误处理

  无论哪个对象里面抛出异常,都可以通过最后一个对象 catch 来捕获异常,通过这种方式可以将所有 Promise 对象的错误合并到一个函数来处理,这样就解决了每个任务都需要单独处理异常的问题。

  之所以可以使用最后一个对象来捕获所有异常,是因为 Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被 onReject 函数处理或 catch 语句捕获为止。

3)Promise 与微任务

  Promise 之所以要使用微任务是由 Promise 回调函数延迟绑定技术导致的。

  下面用一个自定义的 Bromise 来实现Promise。


function Bromise(executor) {
    var onResolve_ = null
    var onReject_ = null
    //模拟实现resolve和then,暂不支持rejcet
    this.then = function (onResolve, onReject) {
        onResolve_ = onResolve
    };
    function resolve(value) {
        //setTimeout(()=>{
            onResolve_(value)
        //},0)
    }
    executor(resolve, null);
}
function executor(resolve, reject) {
    resolve(100)
}
//将Promise改成自定义的Bromsie
let demo = new Bromise(executor)
function onResolve(value){
    console.log(value)
}
demo.then(onResolve)


  执行这段代码,发现执行出错,输出的内容是:

Uncaught TypeError: onResolve_ is not a function

  之所以出现这个错误,是由于 Bromise 的延迟绑定导致的,在调用到 onResolve_ 函数的时候,Bromise.then 还没有执行,所以执行上述代码的时候,当然会报错。

  要让 resolve 中的 onResolve_ 函数延后执行,可以在 resolve 函数里面加上一个定时器,也就是取消代码中的注释。

  但是采用定时器的效率并不是太高,好在有微任务,所以 Promise 又把这个定时器改造成了微任务。


五、async/await


  使用 promise.then 仍然是相当复杂,虽然整个请求流程已经线性化了,但是代码里面包含了大量的 then 函数,使得代码依然不是太容易阅读。

  基于这个原因,ES7 引入了 async/await,这是 JavaScript 异步编程的一个重大改进,提供了在不阻塞主线程的情况下用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰。

1)生成器

  生成器(Generator)函数是一个带星号函数,而且可以暂停和恢复执行。


function* genDemo() {
    console.log("开始执行第一段")
    yield 'generator 2'
    console.log("开始执行第二段")
    yield 'generator 2'
    console.log("开始执行第三段")
    yield 'generator 2'
    console.log("执行结束")
    return 'generator 2'
}
console.log('main 0')
let gen = genDemo()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')


  下面来看看生成器函数的具体使用方式:

  (1)在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。

  (2)外部函数可以通过 next 方法恢复函数的执行。

2)协程

  要搞懂函数为何能暂停和恢复,那首先要了解协程的概念。协程是一种比线程更加轻量级的存在。

  可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。

  如果从 A 协程启动 B 协程,就把 A 协程称为 B 协程的父协程。

  协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

  结合上面那段代码的执行过程,画出了下面的“协程执行流程图”。


48.png


  (1)通过调用生成器函数 genDemo 来创建一个协程 gen,创建之后,gen 协程并没有立即执行。

  (2)要让 gen 协程执行,需要通过调用 gen.next。

  (3)当协程正在执行的时候,可以通过 yield 关键字来暂停 gen 协程的执行,并返回主要信息给父协程。

  (4)如果协程在执行期间,遇到了 return 关键字,那么 JavaScript 引擎会结束当前协程,并将 return 后面的内容返回给父协程。

  为了直观理解父协程和 gen 协程是如何切换调用栈的,可以参考下图:


49.png

  在 JavaScript 中,生成器就是协程的一种实现方式。


//foo函数
function* foo() {
    let response1 = yield fetch('https://www.geekbang.org')
    console.log('response1')
    console.log(response1)
    let response2 = yield fetch('https://www.geekbang.org/test')
    console.log('response2')
    console.log(response2)
}
//执行foo函数的代码
let gen = foo()
function getGenPromise(gen) {
    return gen.next().value
}
getGenPromise(gen).then((response) => {
    console.log('response1')
    console.log(response)
    return getGenPromise(gen)
}).then((response) => {
    console.log('response2')
    console.log(response)
})

  foo 函数是一个生成器函数,在 foo 函数里面实现了用同步代码形式来实现异步操作。

  不过通常,把执行生成器的代码封装成一个函数,并把这个执行生成器代码的函数称为执行器(可参考著名的 co 框架)。

3)async/await

  async/await 技术背后的秘密就是 Promise 和生成器应用,往低层说就是微任务和协程应用。

  根据 MDN 定义,async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。


async function foo() {
    console.log(1)
    let a = await 100
    console.log(a)
    console.log(2)
}
console.log(0)
foo()
console.log(3)


  先站在协程的视角来看看这段代码的整体执行流程图:


50.png


  当执行到await 100时,会默认创建一个 Promise 对象。

let promise_ = new Promise((resolve,reject){
  resolve(100)
})

  然后 JavaScript 引擎会暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时会将 promise_ 对象返回给父协程。

  接下来继续执行父协程的流程,打印出 3。随后父协程将执行结束,在结束之前,会进入微任务的检查点,然后执行微任务队列。

  最后触发 promise_.then 中的回调函数,将主线程的控制权交给 foo 函数的协程,并同时将 value 值传给该协程,并继续执行后续打印语句。


六、渲染流水线


1)含有 CSS

  先结合下面代码来看看最简单的渲染流程:


<html>
<head>
    <link href="theme.css" rel="stylesheet">
</head>
<body>
    <div>geekbang com</div>
</body>
</html>


51.png


  请求 HTML 数据和构建 DOM 中间有一段空闲时间,这个空闲时间有可能成为页面渲染的瓶颈。

  在 DOM 构建结束之后、theme.css 文件还未下载完成的这段时间内,渲染流水线无事可做,因为下一步是合成布局树,而合成布局树需要 CSSOM 和 DOM,所以这里需要等待 CSS 加载结束并解析成 CSSOM。

  CSSOM 体现在 DOM 中就是document.styleSheets,和 DOM 一样,CSSOM 也具有两个作用。

  (1)第一个是提供给 JavaScript 操作样式表的能力。

  (2)第二个是为布局树的合成提供基础的样式信息。

2)含有JavaScript和CSS

  这段代码是我在开头代码的基础之上做了一点小修改,在 body 标签内部加了一个简单的 JavaScript。



<html>
<head>
    <link href="theme.css" rel="stylesheet">
</head>
<body>
    <div>geekbang com</div>
    <script>
        console.log('time.geekbang.org')
    </script>
    <div>geekbang com</div>
</body>
</html>


52.png


  在执行 JavaScript 脚本之前,如果页面中包含了外部 CSS 文件的引用,或者通过 style 标签内置了 CSS 内容,那么渲染引擎还需要将这些内容转换为 CSSOM,因为 JavaScript 有修改 CSSOM 的能力,所以在执行 JavaScript 之前,还需要依赖 CSSOM。也就是说 CSS 在部分情况下也会阻塞 DOM 的生成。

3)白屏

  从发起 URL 请求开始,到首次显示页面的内容,在视觉上经历的三个阶段。

  (1)第一个阶段,等请求发出去之后,到提交数据阶段,这时页面展示出来的还是之前页面的内容。

  (2)第二个阶段,提交数据之后渲染进程会创建一个空白页面,通常把这段时间称为解析白屏,并等待 CSS 文件和 JavaScript 文件的加载完成,生成 CSSOM 和 DOM,然后合成布局树,最后还要经过一系列的步骤准备首次渲染。

  (3)第三个阶段,等首次渲染完成之后,就开始进入完整页面的生成阶段了,然后页面会一点点被绘制出来。

  要想缩短白屏时长,可以有以下策略:

  (1)通过内联 JavaScript、内联 CSS 来移除这两种类型的文件下载,这样获取到 HTML 文件之后就可以直接开始渲染流程了。

  (2)还可以尽量减少文件大小,比如通过 webpack 等工具移除一些不必要的注释,并压缩 JavaScript 文件。

  (3)将一些不需要在解析 HTML 阶段使用的 JavaScript 标记上 sync 或者 defer。

  (4)对于大的 CSS 文件,可以通过媒体查询属性,将其拆分为多个不同用途的 CSS 文件。


七、页面性能


  页面优化,其实就是要让页面更快地显示和响应。

  通常一个页面有三个阶段:加载阶段、交互阶段和关闭阶段。

  (1)加载阶段,是指从发出请求到渲染出完整页面的过程,影响到这个阶段的主要因素有网络和 JavaScript 脚本。

  (2)交互阶段,主要是从页面加载完成到用户交互的整合过程,影响到这个阶段的主要因素是 JavaScript 脚本。

  (3)关闭阶段,主要是用户发出关闭指令后页面所做的一些清理操作。

1)加载阶段

  把这些能阻塞网页首次渲染的资源称为关键资源,例如JavaScript、首次请求的 HTML 资源文件、CSS 文件。

  基于关键资源,继续细化出来三个影响页面首次渲染的核心因素。

  (1)第一个是关键资源个数。

  (2)第二个是关键资源大小。

  (3)第三个是请求关键资源需要多少个 RTT。

  RTT(Round Trip Time) 就是这里的往返时延。它是网络中一个重要的性能指标,表示从发送端发送数据开始,到发送端收到来自接收端的确认,总共经历的时长。

  总的优化原则就是减少关键资源个数,降低关键资源大小,降低关键资源的 RTT 次数。

  (1)将 JavaScript 和 CSS 改成内联的形式。如果 JavaScript 代码没有 DOM 或者 CSSOM 的操作,则可以改成 async 或者 defer 属性;同样对于 CSS,如果不是在构建页面之前加载的,则可以添加媒体取消阻止显现的标志。

  (2)压缩 CSS 和 JavaScript 资源,移除 HTML、CSS、JavaScript 文件中一些注释内容,也可以通过取消 CSS 或者 JavaScript 中关键资源的方式。

  (3)通过减少关键资源的个数和减少关键资源的大小搭配来实现。除此之外,还可以使用 CDN 来减少每次 RTT 时长。

2)交互阶段

  谈交互阶段的优化,其实就是在谈渲染进程渲染帧的速度,因为在交互阶段,帧的渲染速度决定了交互的流畅度。

  先来看看交互阶段的渲染流水线(如下图)。


53.png


  大部分情况下,生成一个新的帧都是由 JavaScript 通过修改 DOM 或者 CSSOM 来触发的。还有另外一部分帧是由 CSS 来触发的。

  一个大的原则就是让单个帧的生成速度变快。所以,下面就来分析下在交互阶段渲染流水线中有哪些因素影响了帧的生成速度以及如何去优化。

  (1)减少 JavaScript 脚本执行时间,不要一次霸占太久主线程。

  一种策略是将一次执行的函数分解为多个任务,另一种是把一些和 DOM 操作无关且耗时的任务放到 Web Workers 中去执行。

  (2)避免强制同步布局。

  通过 DOM 接口执行添加元素或者删除元素等操作后,是需要重新计算样式和布局的,不过正常情况下这些操作都是在另外的任务中异步完成的,这样做是为了避免当前的任务占用太长的主线程时间。


54.png


相关文章
|
1月前
|
缓存 JavaScript
vue阻止浏览器刷新和关闭页面提示
使用场景:在使用vuex进行缓存管理时,页面的缓存会随着页面关闭而消失,如果缓存动作仍在进行中,关闭页面会导致数据丢失,此时需要阻止页面关闭
51 3
|
2月前
|
存储 监控 安全
360 企业安全浏览器基于阿里云数据库 SelectDB 版内核 Apache Doris 的数据架构升级实践
为了提供更好的日志数据服务,360 企业安全浏览器设计了统一运维管理平台,并引入 Apache Doris 替代了 Elasticsearch,实现日志检索与报表分析架构的统一,同时依赖 Doris 优异性能,聚合分析效率呈数量级提升、存储成本下降 60%....为日志数据的可视化和价值发挥提供了坚实的基础。
360 企业安全浏览器基于阿里云数据库 SelectDB 版内核 Apache Doris 的数据架构升级实践
|
2月前
|
数据采集 Web App开发 JSON
浏览器插件:WebScraper基本用法和抓取页面内容(不会编程也能爬取数据)
本文以百度为实战案例演示使用WebScraper插件抓取页面内容保存到文件中。以及WebScraper用法【2月更文挑战第1天】
126 2
浏览器插件:WebScraper基本用法和抓取页面内容(不会编程也能爬取数据)
|
2月前
|
存储 缓存 前端开发
浏览器缓存工作原理是什么?
浏览器缓存工作原理是什么?
|
18天前
【超实用】Angular如何修改当前页面网页浏览器url后面?param1=xxx&param2=xxx参数(多用于通过浏览器地址参数保存用户当前操作状态的需求),实现监听url路由切换、状态变化。
【超实用】Angular如何修改当前页面网页浏览器url后面?param1=xxx&param2=xxx参数(多用于通过浏览器地址参数保存用户当前操作状态的需求),实现监听url路由切换、状态变化。
|
19天前
|
搜索推荐 前端开发 UED
html页面实现自动适应手机浏览器(一行代码搞定)
html页面实现自动适应手机浏览器(一行代码搞定)
19 0
|
1月前
|
Web App开发 缓存 网络协议
|
2月前
|
存储 安全 前端开发
浏览器跨窗口通信:原理与实践
浏览器跨窗口通信:原理与实践
44 0
|
2月前
|
消息中间件 JavaScript 前端开发
前端秘法进阶篇----这还是我们熟悉的浏览器吗?(浏览器的渲染原理)
前端秘法进阶篇----这还是我们熟悉的浏览器吗?(浏览器的渲染原理)
|
3月前
|
搜索推荐 前端开发 UED
html页面实现自动适应手机浏览器(一行代码搞定)
html页面实现自动适应手机浏览器(一行代码搞定)
62 0

热门文章

最新文章