Javascript.RU

Оптимизации стандартного режима

Оптимизации Google Closure Compiler делятся на два типа.

Первый тип - стандартные. Они задуманы как безопасные. Эти оптимизации делают внутренние преобразования функций и кода, которые не должны влиять на результат выполнения.

Их знание помогает писать код так, чтобы компилятор оптимизировал его как можно более эффективно.

Эти оптимизации применяются в режиме --compilation_level SIMPLE_OPTIMIZATIONS, установленном по умолчанию.

Главная операция в этом режиме - переименование локальных переменных.

Например, если внутри функции объявлено длинное имя переменной nodeToHide:

function hide(nodeToHide) {
  nodeToHide.style.display = 'none'
}

... То ничего страшного не случится, если заменить его на более короткое n:

function hide(n) {
  n.style.display = 'none'
}

А размер функции при этом уменьшился.

Перед переименованием оптимизатор сортирует все переменные по частоте применения, так что более часто используемые переменные получат более короткие имена.

Эта оптимизация составляет основу работы современных оптимизаторов Javascript, включая ShrinkSafe, YUI Compressor и Google Closure Compiler.

К совсем безопасным оптимизациям также относятся вырезание из кода комментариев и лишних пробелов.

Далее мы рассмотрим список дополнительных оптимизаций, применяемых Google Closure Compiler.

Все вычисления, которые можно произвести тут же, производятся и вместо них подставляется результат.

Closure compiler ищет константы, между которыми производится какая-нибудь операция, и выполняет эти вычисления.

Например: 1+2 станет 3, "my"+'string' станет "mystring", 1 && 0 станет просто 0.

До оптимизации:

function test(name, value) {
   run(name, 'my'+'string', 60*60*5, 1 && 0, value && 0)
}

После:

function test(a, b) {
  run(a, "mystring", 18000, 0, b && 0)
}

Обратите внимание, что последний аргумент run остался неизменным: b && 0, хотя по логике его заведомо можно заменить на 0. Это потому, что объединение констант оперирует только с константами.

Рассмотрим следующий код:

function test(nodeId) {
  var elem = document.getElementsById(nodeId)
}

После сжатия:

function test(a) {
  a = document.getElementsById(a)
}

Как видите, в исходном коде было 2 переменные: nodeId и elem, а в получившемся - осталась только одна.

Это потому, что google closure compiler ищет и использует заново те имена переменных, для которых допустимо повторное использование. Например:

var x = 1
print(x)
var y = 2
print(y)

Переменная x, начиная со 2й строки, не используется, и поэтому этот код вполне можно заменить на:

var a = 1
print(a)
a = 2
print(a)

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

В чем еще бонус уменьшения количества переменных?

Допустим, у нас было 40 локальных переменных. Каждую хотелось бы заменить на короткое имя, желательно в один символ. Итого, для переименования потребовалось бы 40 символов. Столько нету, поэтому некоторые переменные будут заменены на сочетания из двух символов. А после уменьшения количества переменных - их может остаться всего 20, так что вполне хватит по одному символу на каждую.

При выполнении этой оптимизации google code compiler опирается на построенный по коду граф взаимодействий, в котором содержится информация о присвоении значений переменным.

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

Посмотрим на это в следующем примере:

function test(node) {
  node.innerHTML = 'newValue'
  node = 123 // лишнее присваивание
}

Стало:

function test(a) {
  a.innerHTML = "newValue"
}
;

Переменная node не используется после присваивания, поэтому присваивание было удалено.

Другой пример:

function test(node) {
  node.innerHTML = 'newValue'
  node = createElement('div')
}

Стало:

function test(a) {
  a.innerHTML = "newValue";
  createElement("div")
}

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

Аннотация @nosideeffects

В документации к google closure compiler есть возможность указывать отсутствие side effects путем аннотации:

/** @nosideeffects */
function createElement(name) {
  return document.createElement(name)
}
 
function test(node) {
  node.innerHTML = 'newValue'
  node = createElement('div')
}

Естественно было бы ожидать, что после такого описания вызов node = createElement('div') будет полностью удален из кода как неиспользуемый и без дополнительных эффектов.

Однако, на момент написания статьи эта аннотация никак не влияет на оптимизацию.

В исходных кодах google closure compiler есть соответствующие классы и методы (см. computeFunctionSideEffects и markPureFunctions), которые могли бы помочь, но они никак не используются. Возможно, это в связи с невычищенными багами.

Эта простая оптимизация объединяет несколько объявлений var в одно.

До оптимизаии:

function test() {
  var x = 1
  var y = 2

  return x+y
}

После:

function test() {
  var a = 1, b = 2;
  return a + b
}

Эта оптимизация отыскивает все обращения к свойствам вида object["prop"] и меняет их на object.prop.

До:

function test(node) {
  node["innerHTML"] = 'test'
}

После:

function test(a) {
  a.innerHTML = "test"
}

Google Closure Compiler вовсю использует тернарный оператор и вызов через && для укорачивания операций if:

Было:

if (a) {
  alert(1)
} else {
  alert(2)
}

Станет:

a?alert(1):alert(2);

Еще пример.
Было:

if (a) {
  alert(1)
}

Станет:

a&&alert(1);

Такому преобразованию подвергаются только однострочные блоки if..else.

Все метки анализируются. Если метка используется, она переименовывается на более короткую. Если нет - удаляется.

Например:

function test() {
  label: { 
    var a = 5
  }
}

function test2() {
  label: for(var i=0;i<5; i++) { 
    var a = 5
  }
}

function test3() {
  label: for(var i=0;i<5; i++) { 
    var a = 5
    break label;
  }
}

После оптимизации:

function test() {
  var a = 5
}
function test2() {
  var a = 0;
  for(;a < 5;a++)var b = 5
}
function test3() {
  var a = 0;
  a:for(;a < 5;) {
    a = 5;
    break a
  }
}

Как видно, из первых двух функций метка была удалена, т.к. не используется. А в последней функции - сохранилась, но стала более короткой.

Весь код после оператора return удаляется.

Кроме того, удаляются заведомо безопасные фрагменты, например Class.prototype.property; или true;.

Например, до оптимизации:

function test() {
  test.a.b; 

  alert("test") 

  return;

  window.callMe()
}

После:

function test() {
  alert("test")
}

Google closure compiler удалил обращение к свойству test.a.b и заведомо невыполнимый код после return.

Эта оптимизация не совсем безопасна, т.к., например, Firefox позволяет определять функции-геттеры для свойств. При этом обращение object.prop будет вызывать функцию-геттер, которая потенциально может что-то сделать.

Но если вы не используете геттеры, либо ваши геттеры не имеют side effects (как и должно быть) - то для вас такая оптимизация безопасна.

Кроме того, оптимизатором могут быть удалены заведомо лишние блоки try..catch и объявленные, но не используемые переменные внутри функций.

Также будут удалены блоки вида:

if (0) { .... }

Это довольно удобно, поскольку такого вида проверки часто используются для комментариев. Например, пусть у нас был такой код:

function Animal(name) {
  if(name) {
     doThis()
  } else {
     doThat()
  }
  this.name = name
}

Нам захотелось по-быстрому отключить ветку if(name), но менять структуру кода не хотим. Обычно это делается добавлением && 0 к условию:

function Animal(name) {
  if(name && 0) {
     doThis()
  } else {
     doThat()
  }
  this.name = name
}

После оптимизации этот код превратится в:

function Animal(a) {
  doThat();
  this.name = a
}

Ветка if(name && 0) была удалена вместе с конструкцией if.

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

Google Closure Compiler меняет форму объявления функции, делая его короче:

До:

var test = function() { return 1 }

После:

function test() {  return 1 }

Эта оптимизация потенциально небезопасна, так как эти две формы записи функции неэквивалентны.

Функция, объявленная как function name(...) {...}, видна везде в области видимости, а объявленная через присвоение переменной - лишь после присвоения.

С другой стороны, на практике эта "багофича" компилятора вреда не приносит. Расширилась немного область видимости функции, ну и что?

Компилятор старается сделать эту оптимизацию максимально безопасно. Например, если одна и та же функция объявляется 2 раза - он переименует только первое объявление.

function go(node) {

	var process = function(node) {
		node.style.display = 'none'
	}

	process(node)

	var process = function(node) {
		node.innerHTML = 'newText'
	}

	process(node)

}

После сжатия:

function go(c) {
  function a(b) {
    b.style.display = "none"
  }
  a(c);
  a = function(b) {
    b.innerHTML = "newText"
  };
  a(c)
}
;

Как видно, переименовано только первое объявление, так что конфликтов не возникает.

Если переменные-функции объявляются в более сложной структуре, внутри вложенных if..else и т.п., то переименования может вообще не быть.

Использование with всегда было проблемой оптимизаторов javascript. Эта конструкция создает область видимости из объекта. В результате любые обращения к переменным внутри with могут как относиться как к объекту, так и к переменным вне with.

Например:

function changePosition(elem) {
  with (elem.style) {
    position = 'absolute'
  }
}

C виду - простая функция. Что делает - тоже понятно: меняет elem.style.position.

Но на этапе компиляции и анализа кода - непонятно, будет ли position меняться в самом объекте, а не в глобальной области.

Хотя это знание потенциально можно было бы извлечь из типа elem (Closure Compiler умеет обращаться с типами), но вообще говоря - не известно точно, какое свойство на момент выполнения этого кода будет в объекте, а какое - нет.

Так как компилятор не знает наверняка, с каким объектом будет иметь дело with, и какие у этого объекта будут свойства, то он не может переименовать position.

После сжатия:

function changePosition(a) {
  with(a.style)position = "absolute"
}
;

Однако, если здесь же объявлена локальная переменная, то электронные мозги клинит, и код преобразуется вот так.

Было:

function changePosition(elem) {
  var position

  with (elem.style) {
    position = 'absolute'
  }

}

Станет:

function changePosition(a) {
  with(a.style);
}
;

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

Чтобы не напороться на эту ошибку и другие подобные грабли - проще всего не применять конструкцию with. Google Closure Compiler выводит warning, если видит with.

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

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

Однако, Google Closure Compiler переименовывает переменные, не обращая внимания на eval.

Это нужно иметь в виду, если ваш eval меняет локальные переменные или выполняет (формат JSONP) функции.

Условная компиляция (Conditional Compilation) - фишка IE, которая позволяет выполнить разный код в зависимости от ОС и версии браузера.

Как правило, они используются для определения версии браузера:

Пример: Определение версии через CC
function getBrowser() {
	var browser = navigator.userAgent
/*@cc_on
   /*@if (@_win32)
	browser = "ieWin"
   @else @*/
	browser = "ieNoWin"
   /*@end
@*/
	return browser
}

Собственно, этот способ надежнее разбора userAgent, т.к. в userAgent прописываются разные плагины, что иногда приводит к глюкам распознавания.

Google Closure Compiler понятия не имеет об условной компиляции (в отличие, например, от YUI compressor). Поэтому он просто удалит все от /* до */:

function getBrowser() {
  var a = navigator.userAgent;
  return a = "ieNoWin"
}
;

Это совсем не тот результат, который мы хотели рассчитывали получить.

Поэтому вывод здесь простой: не стоит жать скрипт с условной компиляцией google closure compiler'ом.

В этой статье описаны основные серьезные оптимизации, которые являются "визитной карточкой" стандартного режима сжатия. Кроме них, google closure compiler проводит еще ряд мелких изменений, например:

  • Убирает лишние кавычки у ключей

    {"prop" : "val" }   =>  {prop:"val"}
    
  • Упрощает пустые вызовы Array/Object
    a = new Array()   =>  a = []
    o = new Object()  => o = {}
    
  • Убирает лишние апострофы
    a = "a'c"  =>  a="a'c"
    
  • ...И еще некоторые мелкие изменения кода...

Google Closure Compiler в стандартном режиме работает почти безопасно (если не использовать with/eval/CC).

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

Можно смело использовать этот режим как замену вашему привычному упаковщику.


Автор: Dmitry A. Soshnikov, дата: 30 ноября, 2009 - 15:59
#permalink

Функция, объявленная как function name(...) {...}, видна везде в области видимости, а объявленная через присвоение переменной - лишь после присвоения.

С другой стороны, на практике эта "багофича" компилятора вреда не приносит. Расширилась немного область видимости функции, ну и что?

Кстати, это, действительно, получается небезопасное сжатие, зря они так сделали (даже, несмотря на то, что меняется только первая функция). Функции могут создаваться по условию в if-aх, и в данном случае не учитываются баги реализаций (например, IE), да и всех остальных, кроме Gecko.


Автор: goldserg, дата: 26 апреля, 2010 - 15:11
#permalink

И еще вариант, правда вы меня можете поправить если это не так.

Когда функция объявляется в закрытой области видимости.

var fun;

(function(){
  function main() {
   ....
  }
  fun = function() { return main()};
}())

Автор: Гость (не зарегистрирован), дата: 9 октября, 2013 - 08:20
#permalink

Функция, объявленная как function name(...) {...}, видна везде в области видимости, а объявленная через присвоение переменной - лишь после присвоения.

Я не понял при чем тут область видимости. Если присваивать переменной функцию --- переменная и так будет сразу в области видимости, просто будет undefined.


Автор: goldserg, дата: 26 апреля, 2010 - 14:52
#permalink

document.getElementsById(nodeId)

фунция getElementById


Автор: Гость (не зарегистрирован), дата: 30 мая, 2010 - 21:10
#permalink

Русские символы заменяются кодами, типа \u0418\u0437\u0432.
Это лечится?


Автор: aulizko (не зарегистрирован), дата: 22 декабря, 2010 - 05:22
#permalink

@Гость

Это не баг, это фича.
Вот эти последовательности символов - \u0418\u0437\u0432 - это javascript-escapes symbols, т.е. они гарантировано будут работать в любом окружении и на любом движке.

Ну и кроме того, бывают моменты, когда js-файл лежит не в кодировке utf-8, а, скажем, windows-1251, а страничка в кодировке utf-8.
Предположим, что у нас есть функция, которая создает div с каким-то русским текстом в нем. Если бы вместо этих последовательностей в файле были русские символы, пользователь увидел бы кракозябли. С этими кодами - все нормально работает.
Кстати, не смейтесь, но я такое видел у довольно серьезных компаний-разработчиков, где по 100+ программистов - админы и программисты общаются через менеджеров, итог закономерен.


Автор: Гость (не зарегистрирован), дата: 4 февраля, 2011 - 11:38
#permalink

А если в этом примере:

function Animal(name) {
  if(name && 0) {
     doThis()
  } else {
     doThat()
  }
  this.name = name
}

После оптимизации этот код превратится в:

function Animal(a) {
	  doThat();
	  this.name = a
	}

У нас в функции doThat использовалась name? После оптимизации она пропала.

Ну т.е. общий код такой:

function Animal(name) {
          if(name) {
             doThis()
          } else {
             doThat()
          }
          this.name = name
        }
        
        function doThis() {
          alert(name);
        }
        
        Animal('111');

Автор: lazycommit (не зарегистрирован), дата: 8 декабря, 2011 - 13:00
#permalink

Внимательнее читайте. Условие "откомментировано" с использованием "&& 0"


Автор: ikokostya (не зарегистрирован), дата: 3 ноября, 2011 - 01:34
#permalink

В разделе Мелкие оптимизации

Убирает лишние апострофы

a = "a'c"  =>  a="a'c"

Какая здесь трансформация?


Автор: комментарий (не зарегистрирован), дата: 11 апреля, 2020 - 18:19
#permalink

Это не баг, это фича.
Вот эти последовательности символов - \u0418\u0437\u0432 - это javascript-escapes symbols, т.е. они гарантировано будут работать в любом окружении и на любом движке.

Ну и кроме того, бывают моменты, когда js-файл лежит не в кодировке utf-8, а, скажем, windows-1251, а страничка в кодировке utf-8.
Предположим, что у нас есть функция, которая создает div с каким-то русским текстом в нем. Если бы вместо этих последовательностей в файле были русские символы, пользователь увидел бы кракозябли. С этими кодами - все нормально работает: happy wheels 3D.
Кстати, не смейтесь, но я такое видел у довольно серьезных компаний-разработчиков, где по 100+ программистов - админы и программисты общаются через менеджеров, итог закономерен.


Автор: Гость (не зарегистрирован), дата: 16 апреля, 2022 - 01:10
#permalink

Автор: Гость (не зарегистрирован), дата: 16 апреля, 2022 - 12:57
#permalink

Автор: Гость (не зарегистрирован), дата: 6 октября, 2022 - 09:33
#permalink

Very informative and well-written. This was quite beneficial to my freshly inspired blog. Geometry dash is a 2D game that has been around for a long; there are now many related variants, but I feel this is a good challenge for those who have played it.


Автор: patrickgraham, дата: 3 ноября, 2022 - 13:25
#permalink

Nintendo DS games have been taking the world by storm ever since they were released. And now, with NDS ROMS, you can enjoy all your favorite DS games on your computer or mobile device!

NDS ROMS is a new service that allows you to download and play your favorite DS games on your computer or mobile device. With NDS ROMS, you can enjoy all the best that the Nintendo DS has to offer, without having to lug around a heavy console.

NDS ROMS offers an extensive library of DS games to choose from, so you’re sure to find something that you love. Whether you’re a fan of classic games like Mario Kart or Zelda, or you’re looking for something new and exciting, NDS ROMS has you covered.

Not only does NDS ROMS offer an incredible selection of games, but they’re also affordable. You can buy individual games or purchase a subscription that gives you access to the entire library. And with new titles being added all the time, there’s always something new to discover.

So why wait? Start playing your favorite DS games today with NDS ROMS!


Автор: LOL BEANS (не зарегистрирован), дата: 15 декабря, 2022 - 05:50
#permalink

It's fun, but to play online you must first see an ad that says "with friends", but there is no option for a single player to play with friends. When you play the game lol beans, you can feel the excitement when you participate in the races with many players.


Автор: Гость (не зарегистрирован), дата: 16 января, 2023 - 12:17
#permalink

How do you start a day off? Start with the following games for the most entertaining and exciting moments. Please experience the game for fun terraria


Автор: Гость12 (не зарегистрирован), дата: 6 апреля, 2023 - 12:07
#permalink

Stumble guys was also made available for PC in October 2021, and according to the game's creator, 18 million people play it every day currently.


Автор: rosemaryhatch (не зарегистрирован), дата: 7 апреля, 2023 - 07:55
#permalink

How do you begin your day? Begin with the games listed below for the most interesting and exciting moments. Please play the game for entertainment purposes only. Eggy Car


Автор: dewgv (не зарегистрирован), дата: 13 апреля, 2023 - 13:17
#permalink

You are in for a real treat with this! This is without a doubt the most incredible piece of writing I've ever read. Because of you, it is turning out to be a much better day.
cut mover game


Автор: abel1303 (не зарегистрирован), дата: 18 мая, 2023 - 06:50
#permalink

The tone of your Mini Crossword article was spot-on - neither too dry nor too informal.


Автор: Arthur Sharp (не зарегистрирован), дата: 23 мая, 2023 - 10:53
#permalink

I appreciate you taking soccer random - fantastic two-player game in which players may enjoy many exciting matches as well as realistic ball running, kicking, and going towards the goal.


Автор: lesliedavis (не зарегистрирован), дата: 14 июня, 2023 - 11:54
#permalink

Rahul Gandhi allegedly exclaimed, "China is stealing our land," during an attack on a US facility. Outside of time shooter 2, I've never witnessed claims of sovereignty in action, but now I see them.


Автор: AmaraFitzgerald (не зарегистрирован), дата: 8 августа, 2023 - 10:28
#permalink

Time is of the essence in Geometry Dash Breeze. You have to make every move at the right time so you don't fall over the obstacle. This is like taking advantage of the opportunities in life, especially when they come. randomly.


Автор: IMGLookup is safe (не зарегистрирован), дата: 28 сентября, 2023 - 15:05
#permalink

Thanks for putting up this excellent post. Are you interested in viewing any person's private account on Instagram? Yes, now it is possible. You can look up someone's private account on Instagram using the IGMlookup tool. Igmlookup is a safe tool to view private Instagram profiles. Visit the article for more information about it.


Автор: Гость (не зарегистрирован), дата: 29 сентября, 2023 - 13:49
#permalink

How many Forms of Twitch Alerts are There? There are two main forms of alerts; It is common for streamers to use alerts for various reasons, which has an impact on the types of alerts they select. If you want to know how to Add Twitch Alerts, Then I would suggest you read one blog about Twitch alerts.


Автор: JosephCarter (не зарегистрирован), дата: 3 октября, 2023 - 11:52
#permalink

Snake 3D is developer regularly updates the game, addressing bugs and adding new content.


Автор: Гость (не зарегистрирован), дата: 17 октября, 2023 - 12:57
#permalink

The uniqueness of the information presented here shell shockers sets a high standard for informative content.


Автор: 토토사이트추천 (не зарегистрирован), дата: 14 ноября, 2023 - 07:49
#permalink

Quality posts are essential to invite visitors to the page, and that's what the page offers.토토사이트추천


Автор: sronja (не зарегистрирован), дата: 12 января, 2024 - 13:12
#permalink

Your posting of this top-notch piece is much appreciated. Is it something you're curious about to peek at someone's Instagram private account? Sure, it's definitely doable now. Instagram private accounts can be located with the use of the IGMlookup tool. basketball stars


Автор: antiguans2000, дата: 11 апреля, 2024 - 11:39
#permalink

A customized roof report covering the pre- and post-inspection procedures is included with our roof paint nz service. In addition, we offer basic repair and replacement services for missing or loose tiles and fixes.


Автор: Sabrina Carpenter (не зарегистрирован), дата: 29 июля, 2024 - 07:17
#permalink

I've developed a strange respect for the ball in Slope Game. It's small, but it's mighty, and it's taught me the power of persistence.


Автор: Гость (не зарегистрирован), дата: 19 сентября, 2024 - 12:25
#permalink

Jump, run, and dodge through space in Run 3 Unblocked game, a thrilling endless runner where you face tough obstacles in zero-gravity tunnels with uninterrupted gameplay.


Автор: Yashashree (не зарегистрирован), дата: 16 октября, 2024 - 13:51
#permalink

Jump up and down the challenging slopes in the slope game, where speed and agility determine your success!


Автор: Гость (не зарегистрирован), дата: 17 октября, 2024 - 07:20
#permalink

I just wanted to tell you that I am new to blogging spanish dictionary and really like this blog. I will most likely bookmark your blog. You really have some great stories. Thanks for sharing your blog with us. temple run


Отправить комментарий

Приветствуются комментарии:
  • Полезные.
  • Дополняющие прочитанное.
  • Вопросы по прочитанному. Именно по прочитанному, чтобы ответ на него помог другим разобраться в предмете статьи. Другие вопросы могут быть удалены.
    Для остальных вопросов и обсуждений есть форум.
P.S. Лучшее "спасибо" - не комментарий, как все здорово, а рекомендация или ссылка на статью.
Содержание этого поля является приватным и не предназначено к показу.
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Разрешены HTML-таги: <strike> <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <u> <i> <b> <pre> <img> <abbr> <blockquote> <h1> <h2> <h3> <h4> <h5> <p> <div> <span> <sub> <sup>
  • Строки и параграфы переносятся автоматически.
  • Текстовые смайлы будут заменены на графические.

Подробнее о форматировании

CAPTCHA
Антиспам
6 + 2 =
Введите результат. Например, для 1+3, введите 4.
 
Текущий раздел
Поиск по сайту
Содержание

Учебник javascript

Основные элементы языка

Сундучок с инструментами

Интерфейсы

Все об AJAX

Оптимизация

Разное

Дерево всех статей

Последние комментарии
Последние темы на форуме
Forum