Etude d'un système de cache des pages
Ressources pour l'optimisation
Une brève discussion avait été proposée quant à la mise en cache des pages dans les
OptimisationWikini. Cette page est destinée à en discuter de manière plus détaillée, et surtout à en évaluer les performances.
NB.: cette page ne concerne en rien la mise en cache des requêtes SQL.
Pour ma part, je regarde un peu tous azimut (comme je suis débutant, je me forme un peu au passage). J'ai notamment regardé les solutions de cache php. Ce
message donne une explication intéressante sur les performances du php.
, un peu copurt, explique le principe des caches.
Encore un
comparatif des solutions de cache. Il y en a qui sont relativement faciles à mettre en place.
Une douzaine de classes de cache sont disponible sur
phpclasses.org.
J'en teste (ou projette d'en tester) actuellement quelques-uns.
- jpcache ne semble donner aucun résultat (mais je ne l'ai inclu que pour /wakka.php). jpcache est un peu trop simple et ne semble pas gérer l'annulation du cache d'une page donnée pour une action donnée. Les performances avaient pourtant l'air aléchantes.
- RFKcache est beaucoup plus poussé et permet notamment d'effacer une page particulière du cache (pas encore testé).
- morecache.
Cas d'utilisation du cache dans le cas d'une page non-interactive (ie : ne possédant d'appel { }).
- L'utilisateur modifie une page non-interactive.
- L'utilisateur "sauve" cette page non-interactive.
- Le système supprime du cache la page DerniersChangements.
- Le système met cette page en cache.
- L'utilisateur demande de visualiser cette page non-interactive.
- Le système propose la page du cache.
--
CharlesNepote
Je vais envisager un système de cache valable pour les pages interactives. A part les actions il y a aussi les inclusions raw qui pourraient être prises en compte, je pense que je ne les mettrais pas en cache, de façon à laisser le
FormatterRaw? le faire éventuellement (dans une prochaine version de
WikiNi bien sûr). --
LordFarquaad
Il y a dans la classe wakka une fonction
GetMicroTime?() pour mesurer le temps d'execution d'un bout de code.
--
DavidDelon
Le système de cache que je propose
J'ai impléménté (en fait c'est surtout recopier le
FormatterWakka? et le remagnier un peu) un formatter
cache.php qui permet de formatter une page normalement sauf qu'il retourne le code php à sauver dans un fichier afin d'utiliser ce dernier directement plutôt que de formatter la page à chaque nouvel affichage. Le gain se fait uniquement côté php car la requête de lecture de la page dans la base de données se fait de façon automatisée bien avant d'avoir sélectionné le handler, or dans le cas présent, seul le
HandlerShow est concerné. (cf
ClasseWikiMethodeRun?)
Fonctionnement
- Il faut spécifier dans wakka.config.php le répertoire où les pages devront être mises en cache (et s'assurer que php a les droits en écriture dessus). Pour désactiver la cache, il suffit de laisser cette valeur vide: 'pagecache_dir' => 'cache/pages/' (le slash '/' à la fin est très important !)
- Lors de la demande d'affichage d'une page, le handler show va tenter d'inclure (via IncludeBuffered) la version en cache de la page (si la mise en cache est activée). S'il n'y arriver pas, il va mettre en cache la page en question, et de nouveau l'inclure. Les fichiers seront sauvés sous la forme "numero_de_version.php" (ceci peut d'ailleurs permettre d'obtenir facilement le numéro de fichier via (int) $nomDuFichier et pourrait servir pour la purge), ce qui permet de mettre en cache indiféremment n'importe quelle version des pages, et de ne pas avoir à se soucier de supprimmer les fichiers en cache lors de l'édition.
- La "fabrication" du fichier à mettre en cache se fait via le formatter cache.php. Celui-ci va construire le contenu à sauver dans un fichier, en utilisant du html pour tout ce qui est statique (typiquement tout ce qui concerne l'apparence de la page ainsi que le texte, les liens statiques etc.) et du php pour tout ce qui est variable (actions, inclusions raw, liens internes, liens interwiki [pour le cas où on modifierait interwiki.conf], les liens forcés [a priori on ne sait pas de quel type de lien il s'agit...]). Il va aussi ajouter en début de contenu la vérification habituelle de la sécurité sur l'existance de la constante 'WIKINI_VERSION'. Comme il s'agit d'un module, seul le HandlerShow sera affecté (car on le modifiera en conséquence)
Todo
- purger les pages en cache lorsque l'on purge la base de données.NB.: on n'est pas obligé de conserver de vieilles versions en cache vu que celles-ci ne sont de toute façon dans la base de données et qu'elles ne sont en général pas consultées fréquemment. On pourrait donc ajouter une valeur de configuration supplémentaire de X jours pour la suppressions des pages en cache [7 jours devrait être sufisant si on conserve toujours la dernière version] et modifier le HandlerShow de sorte qu'il ne remette pas en cache les pages ayant plus de X jours à moins qu'il s'agisse de la dernière version.
- Fichiers concernés: wakka.php, handlers/page/show.php
- supprimmer les pages en cache lorsque l'on supprime une page de la base
- FIchier concerné: wakka.php (DeleteOrphanedPage: une requête de sélection simple préalable à la suppression)
- Permettre éventuellement de régénérer facilement les pages en cache (par exemple en cas de mise à jour de WikiNi)
- ... ?
Changelog
- 2004-01-08: correction d'une importante faille de sécurité: le php et l'asp pouvaient être interprétés dans les pages en cache... (y a-t-il d'autres langages susceptibles de l'être ?)
- Ceci me fait d'ailleurs me poser la question de savoir si ne serait pas mieux que le code résultant du formatter cache soit composé d'une suite d'appels à 'echo'... ce serait probablement plus lent mais tout de même plus sécuritaire...
Sources:
formatters/cache.php (je ne sais pas si je peux le passer sous GPL...)
<?php
/**
* Fichier cache.php: formatter permettant de construire un fichier php
* (basé sur le formatter wakka.php)
* @version $Idv 0.0.1 2005-01-08 $
* @copyright 2002, Hendrik Mans <hendrik@mans.de>
* @copyright 2002, 2003 David DELON
* @copyright 2002, 2003 Charles NEPOTE
* @copyright 2002, 2003 Patrick PAUL
* @copyright 2003 Eric DELORD
* @copyright 2003 Eric FELDSTEIN
* @copyright 2004 Jean Christophe ANDRÉ
* @copyright 2005 Didier LOISEAU
* All rights reserved.
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// This may look a bit strange, but all possible formatting tags have to be in a single regular expression for this to work correctly. Yup!
if (!function_exists("CacheCallback"))
{
function CacheCallback($things)
{
$thing = $things[1];
$result = '';
static $oldIndentLevel = 0;
static $oldIndentLength = 0;
static $indentClosers = array();
static $newIndentSpace = array();
static $br = 1;
global $wiki;
switch ($thing)
{
// convert HTML thingies
case '<':
return '<';
case '>':
return '>';
// styles
// bold
case '**':
static $bold = 0;
return (++$bold % 2 ? '<strong>' : '</strong>');
// italic
case '//':
static $italic = 0;
return (++$italic % 2 ? '<i>' : '</i>');
// underlinue
case '__':
static $underline = 0;
return (++$underline % 2 ? '<u>' : '</u>');
// monospace
case '##':
static $monospace = 0;
return (++$monospace % 2 ? '<tt>' : '</tt>');
// Deleted
case '@@':
static $deleted = 0;
return (++$deleted % 2 ? '<span class="del">' : '</span>');
// Inserted
case '££':
static $inserted = 0;
return (++$inserted % 2 ? '<span class="add">' : '</span>');
// header level 5
case '==':
static $l5 = 0;
$br = 0;
return (++$l5 % 2 ? "<h5>" : "</h5>\n");
// header level 4
case '===':
static $l4 = 0;
$br = 0;
return (++$l4 % 2 ? "<h4>" : "</h4>\n");
// header level 3
case '====':
static $l3 = 0;
$br = 0;
return (++$l3 % 2 ? "<h3>" : "</h3>\n");
// header level 2
case '=====':
static $l2 = 0;
$br = 0;
return (++$l2 % 2 ? "<h2>" : "</h2>\n");
// header level 1
case '======':
static $l1 = 0;
$br = 0;
return (++$l1 % 2 ? "<h1>" : "</h1>\n");
// forced line breaks
case '---':
return "<br />\n";
default:
// urls
if (preg_match("/^([a-z]+:\/\/\S+?)([^[:alnum:]^\/])?$/", $thing, $matches))
{
// Retrieve url and transform it into valid HTML (htmlentities)
$url = htmlentities ($matches[1]);
if (!isset($matches[2])) $matches[2] = '';
return "<a href=\"$url\">$url</a>" . $matches[2];
}
// escaped text
elseif (preg_match("/^\"\"(.*)\"\"$/s", $thing, $matches))
{
return str_replace(array('<?', '?>', '<%', '%>'), array('<?', '?>', '<%', '%>'), $matches[1]);
}
// code text
elseif (preg_match("/^\%\%(.*)\%\%$/s", $thing, $matches))
{
// check if a language has been specified
$code = $matches[1];
if (preg_match("/^\((.*)\)(.*)$/s", $code, $matches))
{
list(, $language, $code) = $matches;
}
else
{
$language = '';
}
// Select formatter for syntaxe hightlighting
if (file_exists("formatters/coloration_" . $language . ".php"))
{
$formatter = "coloration_" . $language;
}
else
{
$formatter = "code";
}
$output = "\n";
return $output;
}
// raw inclusion from another wiki
// (regexp documentation : see "forced link" below)
elseif (preg_match("/^\[\[\|(\S*)(\s+(.+))?\]\]$/", $thing, $matches))
{
list (, $url, , $text) = $matches;
if (!$text) $text = "404";
if ($url)
{
$url .= "/wakka.php?wiki=" . $text . "/raw";
return '<?php echo $this->Format($this->Format(\'' . addslashes($url) . '\', "raw"), "wakka"); ?>';
}
else
{
return "";
}
}
// forced links
// \S : any character that is not a whitespace character
// \s : any whitespace character
elseif (preg_match("/^\[\[(\S*)(\s+(.+))?\]\]$/", $thing, $matches))
{
if (isset($matches[2]) && isset($matches[3]))
{
list (, $url, , $text) = $matches;
}
else
{
list (, $url) = $matches;
}
if ($url)
{
if ($url != ($url = (preg_replace("/@@|££|\[\[/", "", $url))))$result = "</span>";
if (!isset($text)) $text = $url;
$text = preg_replace("/@@|££|\[\[/", "", $text);
return $result . '<?php echo $this->Link(\'' . addslashes($url) . '\', "", \'' . addcslashes($text, '\\\'') . '\'); ?>';
}
else
{
return "";
}
}
// indented text
elseif ((preg_match("/\n(\t+|([ ]{1})+)(-|([[:alnum:]]+)\))?/s", $thing, $matches)) || (preg_match("/^(\t+|([ ]{1})+)(-|([[:alnum:]]+)\))?/s", $thing, $matches) && $brf = 1))
{
// new line
if (isset($brf)) $br = 0;
$result .= ($br ? "<br />\n" : "");
// we definitely want no line break in this one.
$br = 0;
// find out which indent type we want
if (isset($matches[3]))
{
$newIndentType = $matches[3];
}
else
{
$newIndentType = '';
}
if (!$newIndentType)
{
$opener = "";
$br = 1;
}
elseif ($newIndentType == "-")
{
$opener = "<ul>\n";
$closer = "</li>\n</ul>\n";
$li = 1;
}
else
{
$opener = "<ol type=\"" . $matches[4] . "\">\n";
$closer = "</li>\n</ol>\n";
$li = 1;
}
// get new indent level
if (strpos($matches[1], "\t")) $newIndentLevel = strlen($matches[1]);
else
{
$newIndentLevel = $oldIndentLevel;
$newIndentLength = strlen($matches[1]);
if ($newIndentLength > $oldIndentLength)
{
$newIndentLevel++;
$newIndentSpace[$newIndentLength] = $newIndentLevel;
}
elseif ($newIndentLength < $oldIndentLength)
$newIndentLevel = $newIndentSpace[$newIndentLength];
}
$op = 0;
if ($newIndentLevel > $oldIndentLevel)
{
for ($i = 0; $i < $newIndentLevel - $oldIndentLevel; $i++)
{
$result .= $opener;
$op = 1;
array_push($indentClosers, $closer);
}
}
elseif ($newIndentLevel < $oldIndentLevel)
{
for ($i = 0; $i < $oldIndentLevel - $newIndentLevel; $i++)
{
$op = 1;
$result .= array_pop($indentClosers);
if (isset($oldIndentLevel) && isset($li)) $result .= "</li>\n";
}
}
if (isset($li) && $op) $result .= "<li>";
elseif (isset($li))
$result .= "</li>\n<li>";
$oldIndentLevel = $newIndentLevel;
$oldIndentLength = $newIndentLength;
return $result;
}
// new lines
elseif ($thing == "\n")
{
// if we got here, there was no tab in the next line; this means that we can close all open indents.
$c = count($indentClosers);
for ($i = 0; $i < $c; $i++)
{
$result .= array_pop($indentClosers);
$br = 0;
}
$oldIndentLevel = 0;
$oldIndentLength = 0;
$newIndentSpace = array();
$result .= ($br ? "<br />\n" : "\n");
$br = 1;
return $result;
}
// events
elseif (preg_match("/^\{\{(.*?)\}\}$/s", $thing, $matches))
{
if ($matches[1])
return '<?php echo $this->Action(\'' . addcslashes($matches[1], '\\\'') . '\'); ?>';
else
return "{{}}";
}
// interwiki links!
elseif (preg_match("/^" . WN_UPPER . WN_CHAR . "+[:](" . WN_CHAR2 . "*)$/s", $thing))
{
return '<?php echo $this->Link(\'' . addslashes($thing) . '\'); ?>';
}
// wiki links!
elseif (preg_match("/^" . WN_UPPER . WN_LOWER . "+" . WN_UPPER_NUM . WN_CHAR . "*$/s", $thing))
{
return '<?php echo $this->Link(\'' . addslashes($thing) . '\'); ?>';
}
// separators
else if (preg_match("/-{4,}/", $thing, $matches))
{
// TODO: This could probably be improved for situations where someone puts text on the same line as a separator.
// Which is a stupid thing to do anyway! HAW HAW! Ahem.
$br = 0;
return "<hr />\n";
}
// if we reach this point, it must have been an accident.
return $thing;
}
}
}
$text = str_replace("\r", "", $text);
$text = chop($text) . "\n";
$text = preg_replace_callback("/(\%\%.*?\%\%|" . "\"\".*?\"\"|" . "\[\[.*?\]\]|" . "\b[[:lower:]]+:\/\/\S+|" . "\*\*|\#\#|@@|££|__|<|>|\/\/|" . "======|=====|====|===|==|" . "-{4,}|---|" . "\n(\t+|([ ]{1})+)(-|[[:alnum:]]+\))?|" . "^(\t+|([ ]{1})+)(-|[[:alnum:]]+\))?|" . "\{\{.*?\}\}|" . "\b" . WN_UPPER . WN_CHAR . "+[:](" . WN_CHAR2 . "*)\b|" . "\b(" . WN_UPPER . WN_LOWER . "+" . WN_UPPER_NUM . WN_CHAR . "*)\b|" . "\n)/ms", "CacheCallback", $text);
// we're cutting the last <br />
$text = preg_replace('/<br \/>$/', '', trim($text));
echo "<?php\nif (!defined('WIKINI_VERSION'))\n\texit('Accès direct interdit');\n?>$text";
?>
Modification à apporter au
HandlerShow: remplacer la partie "display page" par:
<?php
// display page
// si la cache est activée, on l'utilise:
if ($cachedir = $this->GetConfigValue('pagecache_dir'))
{
// si IncludeBuffered ne retourne pas false, il a su lire la page en cache
if (false !== $page = $this->IncludeBuffered($file = $this->page['id'] . '.php', false, null, $cachedir))
{
echo $page;
}
// sinon cela signifie que la page n'était pas encore en cache, il faut donc l'y mettre
else
{
$fp = fopen($cachedir . $file, 'w+');
fputs($fp, $this->Format($this->page['body'], 'cache'));
fclose($fp);
// inutile de reformatter la page maintenant qu'on a une version disponible en cache...
include $cachedir . $file;
}
}
// la cache est désactivée
else echo $this->Format($this->page["body"], "wakka");
?>
Et voilà, n'oubliez pas d'ajouter la variable de configuration "pagecache_dir" et d'autoriser le répertoire en question en écriture et le tour est joué ;-) Il ne reste qu'à évaluer le gain de performances...
Exemple de version en cache d'une page: l'
ActionBackLinks:
<?php
if (!defined('WIKINI_VERSION'))
exit('Accès direct interdit');
?>Action permettant d'insérer la liste de toutes les pages faisant référence à la page courante. Dans cette présente page, {{backlinks}} donne ceci :<br />
<br />
<?php echo $this->Action('BackLinks'); ?><br />
<br />
Sur la page personnelle d'un utilisateur, cette action affichera aussi les pages dont il est le propriétaire ou le dernier modificateur.<br />
<br />
<h2> Paramètres </h2>
<br />
<h3> Paramètre "page" </h3>
Le paramètre "page" (<?php echo $this->Link('http://www.wikini.net', "", 'WikiNi'); ?> >= 0.4.1) permet de spécifier une page différente de la page courante.<br />
Par exemple <tt>{{backlinks page="PagePrincipale"}}</tt><br />
Ce paramètre peut être utile, par exemple :<br />
<ul>
<li> pour améliorer le <?php echo $this->Link('TableauDeBordDeCeWiki'); ?> : il suffit d'ajouter la liste des pages liées à la page AFaire pour connaître les pages qui doivent faire l'objet d'un travail</li>
<li> pour consolider des données sur une seule page : par exemple la liste des pages liées aux pages EstUnHomme et EstUneFemme</li>
<li> autres ?</li>
</ul>
<br />
<h3> Paramètre "exclude" </h3>
Le paramètre "exclude" (<?php echo $this->Link('http://www.wikini.net', "", 'WikiNi'); ?> >= 0.4.1) permet de spécifier des pages à exclure de la liste des pages qui ont pourtant un lien vers la page de référence.<br />
Il est en effet parfois génant d'afficher la totalité des pages faisant référence à une page. Par exemple, la page AFaire liste la page CharlesNepoteVeilleSurInternet alors que, non seulement cette information n'a pas d'intérêt mais elle pollue en outre la lecture de cette page. Autre exemple, une page MamiFeres a intérêt par exemple à lister CheVal et ElePhant mais pas nécessairement AniMaux qui est une catégorie supérieure. Je suggère donc la création d'un paramètre exclude destiné à exclure certaines pages : par exemple :<br />
<tt>{{backlinks exclude="AniMaux"}}</tt><br />
Le paramètre "exclude" peut contenir plusieurs noms de page séparés par des ";", par exemple : <tt>{{backlinks exclude="AniMaux; PagePrincipale"}}</tt><br />
<br />
<hr />
<?php echo $this->Action('trail toc="ListeDesActionsWikini"'); ?>
--
LordFarquaad [
LordFarquaadASuivre]
serait-il possible d'avoir un cache XHTML strictement statique du wiki en lecture ? De telle manière que le wiki soit accessible en lecture même si PHP et
MySQL sont complètement plantés. La majorité des hits n'étant que des actions de lecture, ça ferait une sacré économie ?
ça pose le problème des actions dans les pages, il faudrait qu'elles soient appliquées lors de la modification d'une page et non à son affichage... --
ActionsReseauxNumeriques
- en fait il suffirait de les appliquer lorsque la page est affichée pour la première fois. Cependant j'ai volontairement choisi de rejetter cette solution, car le résultat d'une action peut différer en fonction de plusieurs choses:
- l'utilisateur: son nom (ActionHeader? principalement), ses droits (notemment pour l'ActionInclude), ses préférences...
- les actions de l'utilisateurs: interractions avec l'action (exemples: ActionTextSearch, ActionInclude et d'autres)
- les autres pages: beaucoup d'actions proposent des interractions avec d'autres pages, et leur résultat dépend donc de leurs modifications/créations/suppressions
- a priori, il est impossible de savoir si une action dépend de ces facteurs, et donc si son résultat peut, ou non être mis en cache. Dans la majorité des cas il ne peut pas... -- LordFarquaad
On basculerait alors en mode dynamique PHP/
MySQL seulement lors des modifications ou des actions spécifiques telles que affichage des difs ou recherche dans le wiki...
ça peut sembler une piste valable ? --
ActionsReseauxNumeriques
- L'idée est bonne mais il ne faut pas non plus vouloir rendre les pages totalement statiques... On notera tout de même que la plupart des pages ne contiennent pas d'action, ce qui les rend donc tout de même statique. Pour ce qui est de faire tourner le wiki en cas de rupture de la connexion à MySQL, cela pourrait être envisagé, cependant la dépendance de WikiNi par rapport à sa base de données est très forte, ce qui fait que ça nécessiterait un travail assez conséquent...
- Pour ce qui est de la mise en cache des pages, il faudrait également modifier la façon de nommer les pages qui sont en cache, étant donné qu'elles sont nommées uniquement par leur numéro de version (impossible de savoir à quelle page correspond un fichier de la cache sans la connexion MySQL)
- En fait pour faire tourner WikiNi sans connexion MySQL, il faudrait plutôt une MiseEnCacheDesRequetes? (ce que je veux bien essayer de faire quand j'aurai le temps). Ceci permettrait certainement aussi un gros gain de performances -- LordFarquaad
- Il existe plusieurs wiki n'utilisant pas de bases de données (tout en ayant la possibilité de comparer les versions archivées de l'historique) je cite: twiki http://twiki.org et pmwiki http://pmwiki.org/wiki/PmWikiFr/PmWikiFr . il y en a d'autres.... il peut etre interessant de voir leur fonctionnement (par contre pour l'avoir testé sur un petit serveur perso, twiki est tres puissant et tres souple, mais particuliereemtn lourd, et une mise en cache des pages est aussi à l'etude pour twiki). spip http://www.spip.net integre un systeme de cache purgeable par l'administrateur) -- Err404?
Gestion de la cache dynamique
J'entends par "
cache dynamique" la cache qui est générée (et gérée) dynamiquement par
WikiNi (ou autre) lors de la génération d'une page à afficher, afin d'éviter de faire plusieurs fois la même chose.
J'ignorer si cela a un autre nom mais j'espère que c'est assez précis comme définition...
État actuel
Dans l'état actuel des choses,
WikiNi fait énormément de requêtes SQL pour générer une page, principalement pour savoir si une page existe lors de la création des liens. Ce procédé étant énormément consomateur de ressources,
il essaie de le réduire en évitant de charger plusieurs fois la même page, en mettant en cache les pages chargées.
On pourrait tout d'abord se poser la question de l'intérêt que cela présente:
- fait-il souvent plusieurs liens vers la même page ?
- a-t-on souvent besoin plusieurs fois du contenu d'une même page ?
- a-t-on vraiment besoin du contenu pour savoir si une page existe ?
- le gain de performences n'est-il pas compensé par la surconsommation de mémoire ?
Évolutions possibles
- Optimisation SQL, avec notemment la possiblité de gérer une cache déterminant uniquement si une page existe ou non
- Mettre en cache le résultat de certaines requêtes SQL. Exemple: la liste des pages existantes, la dernière version de chaque page... Évidemment cela complique la gestion des ajouts/suppressions/modifications de pages, et acroit la consommation de mémoire disque. Par contre cela permettrait probablement de réduire le nombre de requêtes à une dixaine par page ! (au lieu de plus d'une centaine dans certains cas...)
- ...