1 августа 2019 г.

Улучшаем сжатие кода

Материал на этой странице устарел, поэтому скрыт из оглавления сайта.

Здесь мы обсудим разные приёмы, которые используются, чтобы улучшить сжатие кода.

Больше локальных переменных

Например, код jQuery обёрнут в функцию, запускаемую «на месте».

(function(window, undefined) {
  // ...
  var jQuery = ...

  window.jQuery = jQuery; // сделать переменную глобальной

})(window);

Переменные window и undefined стали локальными. Это позволит сжимателю заменить их на более короткие.

ООП без прототипов

Приватные переменные будут сжаты и заинлайнены.

Например, этот код хорошо сожмётся:

function User(firstName, lastName) {
  var fullName = firstName + ' ' + lastName;

  this.sayHi = function() {
    showMessage(fullName);
  }

  function showMessage(msg) {
    alert( '**' + msg + '**' );
  }
}

…А этот – плохо:

function User(firstName, lastName) {
  this._firstName = firstName;
  this._lastName = lastName;
}

User.prototype.sayHi = function() {
  this._showMessage(this._fullName);
}

User.prototype._showMessage = function(msg) {
  alert( '**' + msg + '**' );
}

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

При этом, конечно, нужно иметь в виду общий стиль ООП проекта, достоинства и недостатки такого подхода.

Сжатие под платформу, define

Можно делать разные сборки в зависимости от платформы (мобильная/десктоп) и браузера.

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

Способ 1: локальная переменная

Проще всего это сделать локальной переменной в модуле:

(function($) {

  /** @const */
  var platform = 'IE';

  // .....

  if (platform == 'IE') {
    alert( 'IE' );
  } else {
    alert( 'NON-IE' );
  }

})(jQuery);

Нужное значение директивы можно вставить при подготовке JavaScript к сжатию.

Сжиматель заинлайнит её и оптимизирует соответствующие IE.

Способ 2: define

UglifyJS и GCC позволяют задать значение глобальной переменной из командной строки.

В GCC эта возможность доступна лишь в «продвинутом режиме» работы оптимизатора, который мы рассмотрим далее (он редко используется).

Удобнее в этом плане устроен UglifyJS. В нём можно указать флаг -d SYMBOL[=VALUE], который заменит все переменные SYMBOL на указанное значение VALUE. Если VALUE не указано, то оно считается равным true.

Флаг не работает, если переменная определена явно.

Например, рассмотрим код:

// my.js
if (isIE) {
  alert( "Привет от IE" );
} else {
  alert( "Не IE :)" );
}

Сжатие вызовом uglifyjs -d isIE my.js даст:

alert( "Привет от IE" );

…Ну а чтобы код работал в обычном окружении, нужно определить в нём значение переменной по умолчанию. Это обычно делается в каком-то другом файле (на весь проект), так как если объявить var isIE в этом, то флаг -d isIE не сработает.

Но можно и «хакнуть» сжиматель, объявив переменную так:

// объявит переменную при отсутствии сжатия
// при сжатии не повредит
window.isIE = window.isIE || getBrowserVersion();

Убираем вызовы console.*

Минификатор имеет в своём распоряжении дерево кода и может удалить ненужные вызовы.

Для UglifyJS это делают опции компилятора:

  • drop_debugger – убирает вызовы debugger.
  • drop_console – убирает вызовы console.*.

Можно написать и дополнительную функцию преобразования, которая убирает другие вызовы, например для log.*:

var uglify = require('uglify-js');
var pro = uglify.uglify;

function ast_squeeze_console(ast) {
  var w = pro.ast_walker(),
    walk = w.walk,
    scope;
  return w.with_walkers({
    "stat": function(stmt) {
      if (stmt[0] === "call" && stmt[1][0] == "dot" && stmt[1][1] instanceof Array && stmt[1][1][0] == 'name' && stmt[1][1][1] == "log") {
        return ["block"];
      }
      return ["stat", walk(stmt)];
    },
    "call": function(expr, args) {
      if (expr[0] == "dot" && expr[1] instanceof Array && expr[1][0] == 'name' && expr[1][1] == "console") {
        return ["atom", "0"];
      }
    }
  }, function() {
    return walk(ast);
  });
};

Эту функцию следует вызвать на результате parse, и она пройдётся по дереву и удалит все вызовы log.*.

Карта учебника

Комментарии вернулись :)

перед тем как писать…
  • Если вам кажется, что в статье что-то не так - вместо комментария напишите на GitHub.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.