Сводка дизайна библиотеки кэша браузера (localStorage/indexedDB)

внешний интерфейс JavaScript
Сводка дизайна библиотеки кэша браузера (localStorage/indexedDB)

предисловие

Дизайн кэша браузера всегда был очень важной частью оптимизации веб-производительности, и это также проблема, которую необходимо учитывать, когда сегодня преобладают приложения SPA.Как отличный разработчик интерфейса, чтобы сделать наше приложение более плавным и удобным для пользователей лучше, мы Необходимо сделать хорошую стратегию кэширования браузера.

Каждое веб-приложение должно быть быстрым, особенно для прогрессивных веб-приложений. Под быстротой понимается время, необходимое для отображения значимого контента на экране, обеспечивающего интерактивный опыт менее чем за 5 секунд. И это должно быть очень быстро. Трудно описать, насколько важна надежная высокая производительность. Подумайте об этом так: первая загрузка нативного приложения разочаровывает. Установленные прогрессивные веб-приложения должны обеспечивать пользователям надежную работу.

В этой статье будут представлены некоторые решения для оптимизации веб-производительности, которые сделал автор, и базовый процесс кэширования браузера, и основное внимание будет уделено таким, какКак использовать API кеша браузера для инкапсуляции внешней библиотеки кеша, подходящей для вашей командызначительно улучшить производительность приложений исэкономить деньги для компании.

ты получишь

  • Ознакомьтесь с основным процессом кэширования браузера
  • Базовые решения для оптимизации веб-производительности и ценность, которую стратегии кэширования приносят компании
  • Разработка схемы кэширования на основе LocalStorage и упаковка библиотеки (решение для сохранения данных vuex/redux)
  • Разработка схемы кэширования на основе IndexedDB и инкапсуляция библиотеки
  • В сочетании с библиотекой http-запросов (axios/umi-request) для более тонкой разработки прокси-уровня кэширования.

текст

1. Основной процесс кэширования браузера

Прежде всего, если вы хотите разработать отличную стратегию кэширования, вы должны понимать процесс кэширования браузера.Далее приведена базовая блок-схема, обобщенная автором:

На приведенном выше рисунке показан базовый процесс от запроса браузера до отображения ресурсов. Часть нашей стратегии кэширования может быть оптимизирована из описанного выше процесса. Все мы знаем, что состояние кэша страницы определяется заголовком. Вот несколько концепций. :

1. ETag

Сервер генерирует хеш-строку на основе содержимого ресурса, чтобы определить статус ресурса.Когда пользователь запрашивает первый раз, сервер возвращает браузеру ETag вместе с ресурсом, и браузер отправляет эту строку. вернуться на сервер, когда пользователь снова запросит. , проверьте, был ли изменен ресурс, если нет, используйте кеш напрямую. Конкретный процесс может быть следующим:

Хэши на основе содержимого, как правило, более точны, чем Last-modified.

2. Last-modified

Время последней модификации ресурсов на стороне сервера должно использоваться вместе с контролем кеша, что позволяет проверить, обновляются ли ресурсы на стороне сервера. Когда браузер делает запрос снова, он отправляет на сервер заголовок If-Modified-Since, запрашивая, был ли ресурс изменен после момента времени Last-Modified. Если он не был изменен, верните 304 и используйте кеш; если он был изменен, перейдите к серверу, чтобы снова запросить ресурсы, верните 200 и снова запросите ресурсы.

3. Expires

Время истечения кэша используется для указания времени истечения срока действия ресурса, которое является определенным моментом времени на стороне сервера. То есть Expires=max-age + время запроса, которое необходимо использовать в сочетании с Last-modified.Expires – это поле заголовка ответа веб-сервера, которое указывает браузеру кэшировать непосредственно из браузера до истечения срока действия, когда в ответ на HTTP-запрос. Извлекайте данные без повторного запроса.

4. Максимальный возраст кэш-контроля

Единицей измерения являются секунды, указывающие максимально допустимое время для настройки кэша. После того, как браузер отправит запрос на сервер, браузер больше не будет отправлять запросы на сервер в течение периода максимального возраста. Выше приведены несколько основных концепций кэширования браузера. Дополнительные сведения можно получить в вики, поэтому я не буду представлять их здесь по одному. Далее давайте рассмотрим, как оптимизировать веб-приложения и значение стратегий кэширования. принести в компанию.

2. Базовая схема оптимизации веб-производительности и ценность стратегии кэширования для компании.

Оптимизация веб-характеристик - это старомодная проблема. Я обсуждал этот вопрос несколько лет назад. Автор примерно принимает наличие нескольких часто используемых направлений для оптимизации производительности:

1. Консолидация и сжатие ресурсов.

Например, наши часто используемые инструменты упаковки, такие как gulp или webpack, могут помочь нам сжать js, css, html-код, а также упаковать и объединить js и css разных модулей страниц в один файл Преимущество заключается в том, что он уменьшает HTTP-запросы и уменьшает объем ресурсов. , делая ответ быстрее. Однако все еще существует дефект, заключающийся в том, что объединенный код приведет к тому, что объем ресурсов запроса будет больше, чем у предыдущего субконтракта, поэтому это повлияет на время рендеринга страницы до определенного степени, поэтому здесь необходимо сделать компромисс или частичную загрузку по требованию.

2. Сжатие изображений

Веб-сайт часто занимает больше ресурсов для мультимедийных файлов, таких как изображения, видео, аудио и т. д. Для изображений лучше всего сжимать их заранее при публикации в Интернете.Чтобы уменьшить количество запросов изображений несколько лет назад, обычной практикой были изображения спрайтов.Он состоит в объединении нескольких изображений в большое изображение и отображении разных изображений с помощью фонового позиционирования, но кажется, что в настоящее время он мало используется, и теперь используются больше значков шрифтов, svg или webp , поэтому нам нужно использовать разные в зависимости от разных сценариев.Конечно, текущая основная облачная платформа поддерживает хранилище объектов, которое имеет хорошую оптимизацию медиа-ресурсов.Если позволяют условия, это решение может быть принято, например, Qiniuyun и Ali's объектное хранилище осс.

3. Разумное планирование структуры html кода

Эта оптимизация предназначена в основном для улучшения времени рендеринга страницы.Все мы знаем,что загрузка css и js обычно блокируется.CSS не будет блокировать загрузку js и внешних скриптов, но будет блокировать выполнение js.Если мы поставим css в верхней части тела Внизу, тогда мы можем увидеть дилемму: сначала отображать текст html, а затем рендерить стиль страницы, когда сеть не очень хорошая Если мы поместим скрипт js в голову, он заблокирует рендеринг следующий контент и вызвать некоторые приложения.Ошибка, вызванная тем, что dom еще не сгенерирована, хотя мы можем использовать async и defer, чтобы сделать скрипт асинхронным, но если разные файлы js имеют зависимости, это может вызвать непредвиденные ошибки, поэтому мы можем Практика часто имеет следующую структуру:

<html>
<head>
  <title>趣谈前端</title>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0">
  <link rel="icon" href="/ico.png" type="image/x-icon">
  <link rel="stylesheet" href="/umi.348436c0.css">
<head>
<body>
  <div>...</div>
  // html内容
  
  <script src="/umi.520.js"></script>
</body>
</html>

4. Ленивая загрузка и предварительная загрузка ресурсов

Ленивая загрузка ресурсов может значительно сократить первое экранное время страницы, Мы можем не только использовать ленивую загрузку для картинок, то есть показывать пользователю только картинки в видимой области (хотя ленивая загрузка картинок важнее ), мы также можем Ленивая загрузка контента - это, по сути, особый метод разбиения по страницам. Ленивая загрузка в эпоху jquery является хорошим примером. Конечно, реализовать решение для ленивой загрузки самостоятельно очень просто. Нам нужно только использовать API getBoundingClientRect чтобы сотрудничать с конкретным бизнес-использованием. Тем не менее, платформы на основе контента используются много. Например, наш мобильный телефон перемещается в определенную область, чтобы загрузить больше контента. Хорошим примером является предыдущая реклама автора, механизм сообщения о скрытых точках для заголовка. Общая идея такова:

Предварительная загрузка — это загрузка изображений заранее и их рендеринг непосредственно из локального кеша, когда пользователям нужно их просмотреть. Этот механизм часто противоположен отложенной загрузке. Предварительная загрузка должна обеспечить более плавный пользовательский интерфейс, например, сайты с комиксами, если мы не используйте предварительную загрузку , тогда пользовательский опыт будет довольно плохим при частом переключении изображений, поэтому нам нужно загружать изображения заранее, жертва заключается в том, что пользователь может ждать определенное время, чтобы начать «комическое путешествие».

5. Статические ресурсы используют cdn

Преимущество cdn в том, что он может пробить максимальное количество одновременных запросов на одно и то же доменное имя браузера в следующий раз, так что нет необходимости «стоять в очереди» для повышения скорости загрузки.Все мы запрашиваем максимум 6 одновременные запросы браузера под одним и тем же доменным именем (есть различия между разными браузерами), если элементов более 6, он будет ждать завершения предыдущего запроса, прежде чем продолжить инициацию.Если используется cdn, с одной стороны, это отвечает ближайшими к пользователю ресурсами, с другой стороны, cdn часто находится в другом домене от приложения, поэтому ждать не нужно.Ограничьте количество параллелизма под другими доменами, тем самым ускорив отклик сайта.

6. Кэш браузера

Эта часть представляет собой содержание, обсуждавшееся в предыдущем разделе этой статьи. Я не буду вводить его здесь слишком подробно. Мы также можем использовать localStorage и indexedDB для дальнейшей оптимизации кеша. Мы подробно представим содержание этой части ниже.

7. Оптимизация уровня кода

Уровень кода часто представляет собой способность инженера контролировать код.Хороший инженер часто будет писать код с меньшим количеством кода и более высокой производительностью, например, используя функциональное программирование для оптимизации структуры кода и используя алгоритмы для повышения эффективности выполнения кода js. (Например, сортировка, алгоритмы поиска), если вы хотите узнать об этом больше, вы можете обратиться к двум статьям, которые я написал ранее:

Итак, когда вы пишете код, пожалуйста, постоянно напоминайте себе, выполняется ли сегодня код для тестирования производительности?

8. Используйте технологию веб-воркеров для параллельного выполнения кода js, чтобы уменьшить блокировку

Роль Web Worker заключается в создании многопоточной среды для JavaScript, позволяющей основному потоку создавать рабочие потоки и назначать последним некоторые задачи для выполнения. Пока работает основной поток, рабочий поток работает в фоновом режиме, не мешая друг другу. Подождите, пока рабочий поток завершит задачу вычисления, а затем верните результат в основной поток. Преимущество этого в том, что некоторые ресурсоемкие задачи или задачи с высокой задержкой ложатся на рабочий поток, а основной поток (обычно отвечающий за взаимодействие с пользовательским интерфейсом) будет работать плавно и не будет блокироваться или замедляться.

После того, как рабочая нить будет вновь создана, он всегда будет запущен и не будет прерван действиями по основным потокам (например, пользователь, нажав кнопку, отправляя форму). Это способствует реагированию на сообщение основной нити в любое время. Тем не менее, рабочий более ресурсоемкий и должен быть закрыт, как только он используется.

Я знаю эти знания по оптимизации веб-производительности, мы должны полностью понимать, почему они должны выполнять эту оптимизацию Друзья, у которых есть опыт разработки платформы, могут знать, что платформа контента потребляется медиа-ресурсами, такими как изображения, видео и т. Д. Улучшение взаимодействия с пользователем. склонны помещать эти ресурсы в хранилище сторонней сервисной платформы, которое будет иметь лучшую производительность запросов, но единственные недостатки - это сжигание денег.Каждый запрос - это деньги, хотя и не большие, Однако он также сопротивляется миллионам IP-запросов, поэтому эти хорошие контент-платформы - это как минимум несколько миллионов человек в этом цветке каждый год, особенно по запросу.Так что оптимизируйте веб-сайт, с одной стороны Больше пользователей, лучший пользовательский опыт, также может помочь компании сэкономить трафик, а затем помочь босс, чтобы сэкономить деньги!

Следующий контент научит вас экономить деньги.

3. Дизайн схемы кэширования на основе LocalStorage и инкапсуляция библиотеки (решение сохранения данных vuex/redux)

Свойство localStorage позволяет вам получить доступ к объекту Storage источника Document; сохраненные данные будут сохраняться в сеансе браузера. localStorage похож на sessionStorage, но разница в том, что данные, хранящиеся в localStorage, могут храниться длительное время, а при завершении сеанса страницы, то есть при закрытии страницы, данные, хранящиеся в sessionStorage, будут очищены.

О localStorage написано много статей, и его использование очень простое, поэтому я не буду вдаваться в подробности, но задумывались ли вы об инкапсуляции localStorage самостоятельно? достаточно Простой, нет необходимости инкапсулировать, но учли ли вы, что localStorage является постоянным кешем и не поддерживает время истечения срока действия, поэтому нативное локальное хранилище не может быть удовлетворено в некоторых бизнес-сценариях, поэтому в этом случае вам необходимо реализовать собственную дату истечения срока действия. Библиотека времени localStorage, о том, как реализовать эту функцию, автор также писал ранее статью, которая имеет подробное введение, и может сделать localStorage более мощным, Если вам интересно, вы можете изучить ее:

Автор опубликовал библиотеку в npm, которую можно установить и использовать следующими способами:

import dao from @alex_xu/dao

Или используйте файл umd прямо в теге html, адрес github:Библиотека на основе инкапсуляции localStorage, которая может устанавливать время истечения срока действия.

Библиотека управления состоянием vuex в нашем часто используемом vue, потому что состояние хранится в памяти, поэтому, если мы хотим делать веб-офлайн-приложения или веб-игры, нам часто нужно учитывать постоянное кэширование, тогда мы также можем использовать localStorage для достижения состояния Persistence. функцию, но помните, что место для хранения localStorage составляет 5-10 М. Если есть больший спрос, вы можете использовать indexedDB, представленный ниже, для его достижения.

4. Дизайн схемы кэширования на основе indexedDB и инкапсуляции библиотек

IndexedDB в основном используется для хранения больших объемов структурированных данных (в том числе файлов/BLOB-объектов) на стороне клиента. API использует индексы для обеспечения высокопроизводительного поиска по этим данным. Хотя веб-хранилище полезно для хранения небольших объемов данных, оно менее полезно для хранения больших объемов структурированных данных. IndexedDB — это система транзакционных баз данных, похожая на СУБД на основе SQL. Однако, в отличие от СУБД, использующих фиксированные списки, IndexedDB представляет собой объектно-ориентированную базу данных на основе JavaScript. Это позволяет нам хранить и извлекать объекты, проиндексированные по ключам; любой объект, поддерживаемый алгоритмом структурированного клонирования, может быть сохранен. Нам просто нужно указать схему базы данных, открыть соединение с базой данных, а также получить и обновить серию транзакций.

Когда мы впервые соприкоснулись с indexedDB, нам часто было трудно понять.Нам сначала нужно использовать открытый метод для открытия базы данных, потому что большинство методов indexedDB являются асинхронными, поэтому нам сложно управлять, в том числе создание транзакций, создание таблиц (объектное хранилище для набора данных), добавление объектного хранилища и т. д. Здесь автор не будет вводить конкретное использование indexedDB, но расскажет, как упростить процесс использования indexedDB и инкапсулировать его. в простую и удобную в использовании библиотеку кеша.Следующие инкапсуляции основаны на promises, которые более элегантны в использовании.Следующая идея инкапсуляции:

IndexedDB, с которым мы имеем дело в нашей работе, представляет собой не что иное, как описанные выше операции, поэтому нам нужно извлечь эти API из базового API indexedDB Конкретная реализация выглядит следующим образом:

declare global {
  interface Window { xdb: any; }
}

const xdb = (() => {
  let instance:any = null
  let dbName = ''
  let DB = function(args:any) {
    const cfg = {
      name: args.name || 'test',
      version: args.version || 1,
      onSuccess(e:Event) {
        args.onSuccess && args.onSuccess(e)
      },
      onUpdate(e:Event) {
        args.onUpdate && args.onUpdate(e)
      },
      onError(e:Event) {
        args.onError && args.onError(e)
      }
    }
    this.dbName = args.name
    this.request = null
    this.db = null
    // 打开/创建数据库
    this.init = function() {
      if (!window.indexedDB) {
        console.log('你的浏览器不支持该版本')
        return
      }

      let _this = this
      
      this.request = window.indexedDB.open(this.dbName, cfg.version)
      this.request.onerror = function (event:Event) {
        cfg.onError(event)
      }
      
      
      this.request.onsuccess = function (event:Event) {
        _this.db = _this.request.result
        cfg.onSuccess(event)
      }
      
      this.request.onupgradeneeded = function (event:any) {
        _this.db = event.target.result
        cfg.onUpdate(event)
      }
    }

    this.init()

    // 添加表
    this.createTable = function(name:string, opts:any = {}) {
      let objectStore:any
      if (!this.db.objectStoreNames.contains(name)) {
        opts = {
          keyPath: opts.keyPath,
          indexs: Array.isArray(opts.indexs) ? opts.indexs : []
        }

        // indexs = [{
        //   indexName: 'name',
        //   key: 'name',
        //   unique: true
        // }]

        objectStore = this.db.createObjectStore(name, { keyPath: opts.keyPath })

        if(opts.length) {
          opts.indexs.forEach((item:any) => {
            objectStore.createIndex(item.indexName, item.key, { unique: item.unique })
          })
        }
        return objectStore
      }
    }

    // 访问表中数据
    this.get = function(tableName:string, keyPathVal:any) {
      let _this = this
      return new Promise((resolve, reject) => {
        let transaction = this.db.transaction([tableName])
        let objectStore = transaction.objectStore(tableName)
        let request = objectStore.get(keyPathVal)
  
        request.onerror = function(event:Event) {
          reject({status: 500, msg: '事务失败', err: event})
        }
  
        request.onsuccess = function(event:Event) {
          if (request.result) {
            // 判断缓存是否过期
            if(request.result.ex < Date.now()) {
              resolve({status: 200, data: null})
              _this.del(tableName, keyPathVal)
            }else {
              resolve({status: 200, data: request.result})
            }
          } else {
            resolve({status: 200, data: null})
          }
        }
      })
    }

    // 遍历访问表中所有数据
    this.getAll = function(tableName:string) {
      return new Promise((reslove, reject) => {
        let objectStore = this.db.transaction(tableName).objectStore(tableName)
        let result:any = []
        objectStore.openCursor().onsuccess = function (event:any) {
          let cursor = event.target.result
  
          if (cursor) {
            result.push(cursor.value)
            cursor.continue()
          } else {
            reslove({status: 200, data: result})
          }
        }

        objectStore.openCursor().onerror = function (event:Event) {
          reject({status: 500, msg: '事务失败', err: event})
        }
      })
    }

    // 从表中添加一条数据
    this.add = function(tableName:string, row:any, ex:number) {
      return new Promise((reslove, reject) => {
        let request = this.db.transaction([tableName], 'readwrite')
          .objectStore(tableName)
          .add(Object.assign(row, ex ? { ex: Date.now() + ex } : {}))

        request.onsuccess = function (event:Event) {
          reslove({status: 200, msg: '数据写入成功'})
        }

        request.onerror = function (event:Event) {
          reject({status: 500, msg: '数据写入失败', err: event})
        }
      })
      
    }

    // 更新表中的数据
    this.update = function(tableName:string, row:any) {
      return new Promise((reslove, reject) => {
        let request = this.db.transaction([tableName], 'readwrite')
          .objectStore(tableName)
          .put(row)

        request.onsuccess = function (event:Event) {
          reslove({status: 200, msg: '数据更新成功'})
        }

        request.onerror = function (event:Event) {
          reject({status: 500, msg: '数据更新失败', err: event})
        }
      })
    }

    // 删除某条数据
    this.del = function(tableName:string, keyPathVal:any) {
      return new Promise((resolve, reject) => {
        let request = this.db.transaction([tableName], 'readwrite')
          .objectStore(tableName)
          .delete(keyPathVal)

        request.onsuccess = function (event:Event) {
          resolve({status: 200, msg: '数据删除成功'})
        }

        request.onerror = function (event:Event) {
          reject({status: 500, msg: '数据删除失败', err: event})
        }
      })
    }

    // 清空表数据
    this.clear = function(tableName:string) {
      return new Promise((resolve, reject) => {
        let request = this.db.transaction([tableName], 'readwrite')
          .objectStore(tableName)
          .clear()

        request.onsuccess = function (event:Event) {
          resolve({status: 200, msg: '数据表已清空'})
        }

        request.onerror = function (event:Event) {
          reject({status: 500, msg: '数据表清空失败', err: event})
        }
      })
    }
  }

  return {
    loadDB(args:any) {
      if(instance === undefined || dbName !== args.name) {
        instance = new (DB as any)(args)
      }
      return instance
    }
  }

})()

window.xdb = xdb

export default xdb

Таким образом реализована библиотека indexedDB на основе промисов, которая поддерживает время истечения срока действия.Также очень просто реализовать время истечения срока действия.Это добавить поле времени истечения срока действия внизу при создании строки таблицы.Когда пользователю нужно чтобы установить время истечения для изменения строки, ему нужно только добавить время истечения.Времени достаточно.Когда мы снова получим данные таблицы, нам нужно только определить, истек ли срок изменения строки.Если он истекает, мы можем очистить его и сбросить его.

5. В сочетании с библиотекой http-запросов (axios/umi-request) для более тонкой разработки прокси-уровня кэширования.

Чтобы максимизировать преимущества пространства хранения indexedDB и дополнительно оптимизировать стратегию кэширования, мы можем выполнить перехват кеша.Все мы знаем, что некоторые запросы приложения не нужно получать часто, например, провинциальные и муниципальные каскадные данные, карта местоположения данные или некоторые данные, которые не нужно часто обновлять, если мы можем запросить их только один раз, напрямую использовать данные памяти для следующего запроса и установить время истечения срока действия, после истечения срока действия данные будут повторно запрошены , то можем ли мы выполнить запрос еще раз? Оптимизация? Наше первое впечатление может состоять в том, чтобы написать такой код:

if(!store.get('xx')){
   http.get('xxx').then(res => {
    res && store.set('xx', res, 12 * 60 * 60 * 1000)
  }) 
}

Хотя эта функция может быть достигнута, часто бывает трудно написать аналогичный код для каждого бизнеса, поэтому, будучи программистом, мы можем усердно работать над запросом.У всех нас есть опыт использования библиотеки axios или fetch, мы также подвержены использованию перехватчиков запросов/ответов, поэтому можем ли мы рассмотреть возможность создания уровня перехвата самого запроса? Эффект, которого я хочу добиться, заключается в том, что мы по-прежнему используем запрос в бизнесе, как и раньше, например:

req.get('/getName?type=xxx').then(res)

Тем не менее, кеширование запросов было сделано для нас внутри.Наш req на самом деле не экземпляр axios или fetch, а слой прокси.

Таким образом, мы не можем вносить изменения в исходный метод запроса и можем полностью использовать механизм прокси для размещения нашего прокси в перехватчике запросов и перехватчике ответов.Ключевым моментом является то, что содержимое, хранящееся в базе данных, должно отвечать на сервер, структура контента такая же.

В приведенном выше методе мы можем кэшировать все запросы на получение.Если мы хотим кэшировать только некоторые запросы, на самом деле очень просто использовать описанный выше механизм.Нам нужно только установить белый список кеша и судить в перехватчике запросов, если он в белый список.Просто следуйте логике кэширования.

Таким образом, когда мы снова ищем определенные данные, мы можем получить их напрямую из indexedDB без какого-либо http-запроса, что может сэкономить много трафика для компании.

Что касается инкапсуляции библиотеки indexedDB, я также опубликовал ее на npm и github, и вы можете использовать ее напрямую или выполнять вторичную разработку.

наконец

Если вы хотите узнать большеигра Н5, webpack,node,gulp,css3,javascript,nodeJS,визуализация данных холстаВ ожидании передовых знаний и реальных сражений, добро пожаловать в нашу техническую группу в общедоступном аккаунте «Интересный передний конец», чтобы вместе учиться и обсуждать, а также вместе исследовать границы переднего плана.

больше рекомендаций