Как ускорить сайт с помощью ленивой загрузки изображений

год назад·1 мин. на чтение

Ленивая загрузка изображений — один из самых простых способов ускорить загрузку сайта, поскольку для самой простой реализации ленивой загрузки требуется всего одна строка кода.

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

Что такое ленивая загрузка?

Ленивая загрузка (отложенная загрузка) — это метод, используемый для отсрочки загрузки контента до тех пор, пока он не понадобится. В случае изображений это означает, что изображение не будет загружено до тех пор, пока пользователь не прокрутит до точки, где изображение будет видно на экране. Это отличный способ ускорить работу вашего сайта, поскольку вы загружаете только те изображения, которые пользователь действительно увидит. Это особенно полезно для сайтов с большим количеством изображений, поскольку вы можете сэкономить много пропускной способности, загружая только те изображения, которые пользователь действительно увидит. Если у вас высокая скорость интернета или вы просматриваете сайты только с небольшими, хорошо оптимизированными изображениями, вы можете не увидеть преимущества отложенной загрузки изображений, поскольку вы можете загрузить все изображения почти мгновенно. Но для всех остальных ленивая загрузка изображений меняет играет важную роль. Это касается не только людей со сверхмедленным интернет-соединением. Изображения являются одним из, если не самым большим по размеру контентом, который загрузит ваш пользователь, поэтому, даже если у него быстрое подключение к Интернету, ленивая загрузка изображений все равно может иметь огромное значение для времени загрузки вашего сайта.

Базовая реализация ленивой загрузки

Как я уже упоминал в начале этой статьи, ленивая загрузка изображений так же проста, как добавление одного атрибута к тегу изображения. Атрибуту loading можно присвоить значение lazy, чтобы включить отложенную загрузку изображения. Браузер автоматически определит, когда загружать изображение, в зависимости от того, насколько близко изображение находится на экране.
<img src="image.jpg" loading="lazy" />
Самым большим недостатком этой базовой отложенной загрузки является то, что пользователь увидит пустое место, где должно быть изображение, пока изображение не будет загружено. Это не идеальный пользовательский опыт, поэтому оставшаяся часть этой статьи покажет вам, как воспользоваться отложенной загрузкой, чтобы показать размытое изображение-заполнитель до тех пор, пока не будет загружено полное изображение.

Продвинутая отложенная загрузка

Размытые изображения-заполнители отображаются до тех пор, пока не будет загружено полное изображение, и являются первым шагом к созданию этого расширенного эффекта отложенной загрузки. Чтобы создать размытое изображение-заполнитель, вам просто нужно создать версию изображения со сверхнизким разрешением. Есть много способов сделать это, например, использовать такой сервис, как BlurHash, вручную изменить размер изображения в таком инструменте, как Figma, или автоматически с помощью такого инструмента, как ffmpeg. Мы будем использовать ffmpeg для создания изображений-заполнителей для этой статьи, поскольку это наиболее гибкий вариант, который можно легко автоматизировать. Все, что нужно сделать, это запустить приведенный ниже код в командной строке в каталоге, содержащем изображение, для которого требуется сгенерировать изображение-заполнитель.
ffmpeg -i imageName.jpg -vf scale=20:-1 imageName-small.jpg
При этом будет сгенерировано изображение шириной 20 пикселей, а высота будет автоматически рассчитана для сохранения пропорций исходного изображения. Вы можете изменить ширину на любую другую, но по наблюдениям, 20 пикселей хорошо подходят для большинства изображений и достаточно малы, чтобы загружаться почти мгновенно даже при медленном интернет-соединении. Изображения-заполнители будут примерно по 1КБ каждое.
Следующим шагом является создание div и установка фонового изображения этого div на наше супер маленькое изображение. Это будет изображение-заполнитель, которое будет отображаться до тех пор, пока не будет загружено полное изображение. Наш код будет выглядеть примерно так.
<div class="blurred-img"></div>
.blurred-img {
  background-image: url(imageName-small.jpg);
  background-repeat: no-repeat;
  background-size: cover;
}
Этот div с blurred-img имеет размер в зависимости от размера содержимого в нем. Однако мы можем легко исправить это, добавив img в наш div и убедившись, что он скрыт по умолчанию, чтобы мы никогда не видели его в наполовину загруженном состоянии.
<div class="blurred-img">
  <img src="imageName.jpg" loading="lazy" />
</div>
.blurred-img img {
  opacity: 0;
}
Это даст нам эффект, который мы ищем. Эффект размытия, который мы получаем автоматически, связан с тем, что сверхмаленькое изображение автоматически увеличивается браузером. Если вы хотите добавить больше размытия, вы всегда можете использовать свойство CSS filter, чтобы добавить фильтр к blurred-img.
.blurred-img {
  filter: blur(10px);
}
Вы даже можете сделать еще один шаг вперед, добавив пульсирующую анимацию к изображению-заполнителю. Это сделает еще более очевидным, что изображение загружается.
.blurred-img::before {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0;
  animation: pulse 2.5s infinite;
  background-color: white;
}

@keyframes pulse {
  0% {
    opacity: 0;
  }
  50% {
    opacity: 0.1;
  }
  100% {
    opacity: 0;
  }
}
Теперь единственное, что осталось сделать, это показать основное изображение после его загрузки. Это немного сложнее, чем остальная часть кода, который мы написали до сих пор, поскольку требует от нас использования JavaScript, но все же довольно просто. Нам просто нужно добавить прослушиватель событий к изображению, который будет срабатывать после загрузки изображения.
<div class="blurred-img">
  <img src="imageName.jpg" loading="lazy" />
</div>
const blurredImageDiv = document.querySelector(".blurred-image")
const img = blurredImageDiv.querySelector("img")
function loaded() {
  blurredImageDiv.classList.add("loaded")
}

if (img.complete) {
  loaded()
} else {
  img.addEventListener("load", loaded)
}
.blurred-img {
  background-repeat: no-repeat;
  background-size: cover;
}
.blurred-img::before {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0;
  animation: pulse 2.5s infinite;
  background-color: var(--text-color);
}

@keyframes pulse {
  0% {
    opacity: 0;
  }
  50% {
    opacity: 0.1;
  }
  100% {
    opacity: 0;
  }
}

.blurred-img.loaded::before {
  animation: none;
  content: none;
}

.blurred-img img {
  opacity: 0;
  transition: opacity 250ms ease-in-out;
}

.blurred-img.loaded img {
  opacity: 1;
}
Здесь много кода, поэтому разберем его шаг за шагом. В коде JavaScript мы выбираем blurred-img а затем выбираем img в этом div. Затем мы проверяем свойство complete у img, чтобы увидеть, загрузился ли он еще. Если это так, это означает, что изображение уже загружено, поэтому мы можем просто вызвать функцию loaded. Однако, если это условие ложно, нам нужно добавить прослушиватель событий в img, который будет срабатывать после загрузки изображения, а затем вызывать loaded. loaded просто добавляет класс loaded в blurred-img. В CSS у нас есть несколько изменений в коде. Сначала мы удалили animation/content из элемента blurred-img::before. Это остановит пульсирующую анимацию после загрузки изображения. Мы также добавили transition к элементу img, чтобы он плавно исчезал при добавлении loaded класса в div blurred-img.img Наконец, мы изменяем непрозрачность img на 1, чтобы она была видна при загрузке.

Итоги

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

От JavaScript к Rust: руководство для начинающих

год назад·9 мин. на чтение

Узнаем о сходствах и различиях между Rust и JavaScript и расширим свой инструментарий программирования.

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

Зачем переходить с JavaScript на Rust?

JavaScript, де-факто язык Интернета, является динамичным, гибким и вездесущим. Это язык, который понимают браузеры, что делает его важным инструментом в арсенале веб-разработчика. Несмотря на это, JavaScript имеет свои ограничения, особенно когда речь идет о производительности и безопасности типов. Rust — системный язык программирования, который работает молниеносно, предотвращает ошибки сегментации и гарантирует безопасность потоков. Разработанный Mozilla, Rust неуклонно набирает обороты благодаря своей скорости, надежности, предлагая JavaScript разработчикам прочный мост к системному программированию.

Установка Rust

Чтобы начать работу с Rust, вам необходимо настроить среду разработки. Первым делом необходимо установить необходимые инструменты. Rust предоставляет инструмент rustup, который помогает вам управлять различными версиями компилятора Rust. Выполните следующие действия, чтобы установить Rust:
  1. Посетите официальный сайт Rust: rustup.rs.
  2. Следуйте инструкциям на веб-сайте, чтобы загрузить и установить rustup для вашей операционной системы.
  3. После установки rustup откройте терминал или командную строку и выполните команду rustup update, чтобы убедиться, что у вас установлена последняя версия Rust. После установки rustup у вас будет доступ к трем важным командам: rustup, rustc и cargo. rustup позволяет вам управлять цепочкой инструментов компилятора Rust, rustc — это сам компилятор Rust (хотя вам редко придется использовать его напрямую), а cargo — это менеджер пакетов и система сборки Rust, похожая на npm в JavaScript.

Настройка проекта Rust

Теперь, когда у вас установлен Rust, давайте создадим новый проект Rust. Rust предоставляет мощный инструмент под названием cargo, который упрощает управление проектами и выстраивает процессы. Выполните следующие действия, чтобы создать новый проект Rust:
  1. Откройте терминал или командную строку и перейдите в каталог, в котором вы хотите создать проект.
  2. Выполните команду cargo new my_project_name, заменив my_project_name нужным именем для вашего проекта.
  3. cargo создаст новый каталог с указанным именем проекта, содержащий необходимые файлы и папки для базовой структуры проекта Rust. Внутри каталога проекта вы найдете файл Cargo.toml, который служит конфигурационным файлом проекта, аналогично package.json в JavaScript. Он определяет метаданные проекта, зависимости и другие параметры. Кроме того, вы найдете каталог src, который содержит файлы исходного кода для вашего проекта.

Написание первой программы на Rust

Давайте напишем простую программу «Hello, World!» на Rust, чтобы почувствовать синтаксис языка. Откройте файл src/main.rs в каталоге проекта и замените его содержимое следующим кодом:
fn main() {
    println!("Hello, World!");
}
В Rust функция main является точкой входа для выполнения программы, аналогичная функции main в JavaScript. Макрос println! используется для печати сообщения "Hello, World!" на консоли. Rust использует макросы для генерации кода, что позволяет создавать мощные и гибкие синтаксические расширения. Чтобы запустить программу, откройте терминал или командную строку, перейдите в каталог проекта и выполните команду cargo run. Cargo скомпилирует ваш код и выполнит полученный двоичный файл. Вы должны увидеть сообщение «Hello, World!», напечатанное в консоли. Поздравляю! Вы написали и запустили свою первую программу на Rust. Теперь давайте углубимся в некоторые ключевые понятия и синтаксис в Rust.

Переменные и типы данных в Rust

В Rust переменные по умолчанию неизменяемы (иммутабельны), что означает, что их значения не могут быть изменены после назначения. Однако вы можете использовать ключевое слово mut (mutable, мутабельные) для создания изменяемых переменных. Давайте посмотрим на несколько примеров:
fn main() {
    let name = "Alice"; // Иммутабельная переменная
    let mut age = 30; // Мутабельная переменная
    age += 1; // Инкремент переменной на 1
    println!("Name: {}", name);
    println!("Age: {}", age);
}
В этом примере name — это неизменяемая переменная, содержащая строку, а age — это изменяемая переменная, содержащая целое число. Мы можем изменить значение age с помощью оператора +=. Макрос println! снова используется для отображения значений переменных. Rust предоставляет несколько встроенных примитивных типов данных, включая целые числа, числа с плавающей запятой, логические значения, символы и строки. Ниже приведен обзор некоторых распространенных типов данных:
  • Целочисленные типы: i8, i16, i32, i64, u8, u16, u32, u64
  • Типы чисел с плавающей запятой: f32, f64
  • Логический тип: bool
  • Символьный тип: char
  • Строковый тип: String (изменяемая строка с динамическим размером) и фрагменты строки (&str)
Rust использует вывод типов для переменных на основе их начальных значений. Тем не менее, вы можете явно указать тип, используя let variable: Type = value;. Например:
fn main() {
    let x: i32 = 42;
    let pi: f64 = 3.14;
    let is_true: bool = true;
    let message: &str = "Hello, Rust!";
}

Поток управления

Как и JavaScript, Rust предоставляет различные операторы потока управления, включая выражения if, циклы for и while и выражения match. Давайте рассмотрим каждый из них.

Выражения if

В Rust операторы if — это выражения, которые вычисляют условие и выполняют блок кода на основе результата. Вот пример:
fn main() {
    let number = 5;
    if number % 2 == 0 {
        println!("The number is even");
    } else {
        println!("The number is odd");
    }
}
В этом примере мы проверяем, делится ли переменная number на 2. Если это так, мы печатаем, что число четное; В противном случае мы печатаем, что это нечетное.

Цикл loop

Ключевое слово loop создает бесконечный цикл, который продолжается до явного завершения. Вы можете использовать ключевое слово break, чтобы выйти из цикла. Вот пример:
fn main() {
    let mut count = 0;
    loop {
        println!("Count: {}", count);
        count += 1;
        if count >= 5 {
            break;
        }
    }
}
В этом примере мы инициализируем count равным 0 и используем loop для многократного вывода ее значения. Цикл продолжается до тех пор, пока count не достигнет 5, после чего мы выходим из цикла с помощью оператора break.

Цикл while

Ключевое слово while позволяет создать цикл, который выполняется до тех пор, пока выполняется определенное условие. Вот пример:
fn main() {
    let mut count = 0;
    while count < 5 {
        println!("Count: {}", count);
        count += 1;
    }
}
В этом примере цикл продолжается до тех пор, пока count меньше 5. Мы увеличиваем count на 1 в каждой итерации и выводим его значение.

Выражения match

Выражение match Rust похоже на оператор switch JavaScript, но с более мощными возможностями сопоставления шаблонов (pattern matching). Он позволяет сопоставлять значение с набором шаблонов и выполнять код на основе совпадающего шаблона. Давайте посмотрим на пример:
fn main() {
    let number = 2;
    match number {
        1 => println!("One"),
        2 => println!("Two"),
        3 => println!("Three"),
        _ => println!("Other"),
    }
}
В этом примере мы сопоставляем значение переменной number с различными шаблонами. Если значение равно 1, печатаем "One". Если это 2, мы печатаем "Two". Если это 3, мы печатаем "Three". Если ни один из шаблонов не совпадает, представленный символом подчеркивания _, мы печатаем "Other". Выражения match могут быть более сложными, что позволяет сопоставлять шаблоны с перечислениями, диапазонами и даже захватывать переменные. Это мощная конструкция, которая может обрабатывать широкий спектр сценариев.

Выражения

Еще одним важным аспектом Rust является его природа, основанная на выражениях (expression-based), которая отличается от подхода JavaScript, основанного на утверждениях (statement-based). В Rust почти все является выражением, включая блоки кода. Это позволяет создавать мощные и лаконичные шаблоны. Для примера сравним простой пример потока управления в JavaScript и Rust:
let x = {
    let y = 1 + 1;
    y * y
};
if x == 4 {
    println!("Hello, world!");
}
В этом примере Rust мы определяем блок кода с помощью фигурных скобок {}. Последнее выражение внутри блока (y * y) становится значением всего блока, которое затем присваивается переменной x.

Функции и модули

Функции в Rust определяются с помощью ключевого слова fn, аналогично JavaScript. Давайте посмотрим на пример:
fn greet(name: &str) {
    println!("Hello, {}!", name);
}
fn main() {
    greet("Alice");
    greet("Bob");
}
В этом примере мы определяем функцию greet, которая принимает строковый фрагмент (&str) в качестве параметра и выводит приветственное сообщение. Затем мы дважды вызываем функцию greet в main функции с разными именами. Rust также поддерживает определение модулей, которые помогают организовать и структурировать ваш код. Модули позволяют группировать связанные функции, типы и другие элементы вместе. Ниже приведен пример определения модуля:
mod math {
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    pub fn subtract(a: i32, b: i32) -> i32 {
        a - b
    }
}
fn main() {
    let result = math::add(5, 3);
    println!("Result: {}", result);
}
В этом примере мы определяем math модуль, который содержит две функции: add и subtract. Эти функции выполняют основные арифметические действия. Ключевое слово pub указывает, что функции являются общедоступными и к ним можно получить доступ из-за пределов модуля. В main функции вызываем функцию add из модуля math, передавая аргументы 5 и 3. Результат сохраняется в переменной result, которую затем выводим на консоль. Модули позволяют упорядочить код в логические блоки, упрощая управление и обслуживание более крупных проектов. Вы можете вкладывать модули в другие модули, чтобы создать иерархическую структуру.

Обработка ошибок

Обработка ошибок в Rust является значительным улучшением по сравнению с JavaScript. В то время как JavaScript выбрасывает и ловит ошибки, Rust использует механизм под названием «Result» для обработки ошибок, который представляет собой либо успешное значение (Ok), либо ошибку (Err). Это похоже на использование блоков try/catch в JavaScript.
use std::fs::File;
use std::io::Read;
fn read_file_contents(file_path: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(file_path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

fn main() {
    match read_file_contents("data.txt") {
        Ok(contents) => println!("File contents: {}", contents),
        Err(error) => eprintln!("Error: {}", error),
    }
}
В этом примере мы определяем функцию read_file_contents, которая считывает содержимое файла с указанием пути к нему. Функция возвращает тип Result, где успешным значением является String, содержащая содержимое файла, а тип ошибки — std::io::Error. Внутри функции мы используем оператор ? для распространения любых ошибок, возникающих во время операций с файлами. Если произойдет ошибка, функция немедленно вернет значение ошибки. В main функции мы обрабатываем Result с помощью выражения match. Result в Ok, мы распечатываем содержимое файла. Err, мы печатаем сообщение об ошибке. Используя тип Result и распространение ошибок, Rust обеспечивает обработку ошибок и делает их явными в коде. Это приводит к более надежным программам.

Управление памятью

Одной из выдающихся особенностей Rust является его система, которая обеспечивает безопасность памяти и устраняет распространенные проблемы, такие как исключения null указателей и гонки данных. У каждого значения в Rust есть один владелец. Когда владелец выходит за пределы области, значение очищается. Это избавляет от необходимости сборки мусора. Заимствование позволяет временно заимствовать ссылку на значение, не вступая в права собственности. Rust применяет строгие правила заимствования во время компиляции, чтобы предотвратить гонку данных и недопустимый доступ к памяти. Время жизни гарантирует, что заимствованные ссылки остаются действительными до тех пор, пока они используются. Они позволяют компилятору определять время жизни ссылок и предотвращают висячие ссылки. Вот простой пример, демонстрирующий владение и заимствование в Rust:
fn process_string(s: String) {
    println!("Processing: {}", s);
} // s остается за пределами и уничтожается

fn main() {
    let message = String::from("Hello, Rust!");
    process_string(message); // Владение `message` передается функции
    // println!("Message: {}", message); // Эта строка приводит к ошибке компиляции
}
В этом примере функция process_string становится владельцем String параметра s. Как только вызов функции завершен, s выходит за пределы области и удаляется. Если мы попытаемся использовать message после его передачи функции, это приведет к ошибке компиляции, потому что право собственности было передано. Чтобы позаимствовать ссылку на значение без передачи права собственности, мы можем использовать ссылки (&). Вот пример:
fn process_string(s: &str) {
    println!("Processing: {}", s);
}

fn main() {
    let message = String::from("Hello, Rust!");
    process_string(&message); // Позаимствовать ссылку на `message`
    println!("Message: {}", message); // `message` все еще доступен
}
В этом примере мы определяем функцию process_string для получения ссылки на строковый фрагмент (&str). Вместо того, чтобы передавать String напрямую, мы передаем ссылку на String с помощью оператора &. Это позволяет функции заимствовать значение, не становясь владельцем. Используя ссылки и систему владения, Rust обеспечивает безопасность памяти и устраняет многие распространенные ошибки программирования, связанные с управлением памятью.

Асинхронное программирование

Rust также поддерживает синтаксис async/await, но его работа по стандартизации все еще продолжается, и многие вещи еще не работают или могут привести к ошибкам. Но в настоящее время сообщество предоставляет мощное асинхронное решение, такое как tokio. Например, асинхронные привязки ввода-вывода встроены в Node.js, а в Rust — нет. Но мы можем использовать различные мощные асинхронные среды выполнения, например, использовать async-std для написания простого примера ниже:
use async_std::fs;
#[async_std::main]
async fn main() {
     match fs::read_to_string("./hello.txt").await {
         Ok(file) => println!("read {} chars", file.len()),
         Err(e) => eprintln!("{}", e),
     };
}
Rust «Futures» (аналог Promises) начинают выполняться только тогда, когда они вызываются с .await.

Параллелизм и многопоточность

Rust отлично поддерживает параллельное программирование и предоставляет безопасные абстракции для работы с потоками и общими данными. Модуль std::thread позволяет создавать потоки в Rust и управлять ими. Ниже приведен пример использования потоков для параллельного выполнения:
use std::thread;
fn main() {
    let handle = thread::spawn(|| {
        for i in 1..=5 {
            println!("Thread: {}", i);
        }
    });
    for i in 1..=5 {
        println!("Main: {}", i);
    }
    handle.join().unwrap(); // Ожидаем пока thread закончит выполнение
}
В этом примере мы создаем новый поток с помощью thread::spawn и одновременно выполняем отдельный блок кода. Основной поток и порожденный поток печатают свои соответствующие номера, демонстрируя параллельное выполнение.

Итоги

Rust предлагает мощную и безопасную альтернативу JavaScript, особенно для системного программирования и критически важных для производительности приложений. Хотя переход с JavaScript на Rust может потребовать некоторой корректировки, понимание фундаментальных концепций и синтаксиса Rust поможет вам эффективно использовать его возможности.