Por qué tus páginas con DataTables tardan tanto en cargar

Una página que tarda mucho en cargar es una página mala.

Lo sabés vos y lo saben tus usuarios.

Si tu sitio tarda más de 5 segundos en mostrarse tus visitantes se sentirán así:

Las páginas que usan DataTables se ven bien y son muy funcionales pero pueden tardar una eternidad en desplegarse.

Más de la mitad de los usuarios que no encuentran pronto lo que buscan se van a otro sitio.

Y los que se quedan desconfían de la profesionalidad de quien las desarrolló.

¿Y quién quiere contratar a alguien que entrega trabajos de dudosa calidad?

Más vale dedicar unos minutos a aprender cómo mejorarlas.

Factores que afectan el tiempo de carga de tu página

En su forma más simple, un DataTable puede cargarse usando un arreglo JavaScript.

Basta un código como este:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="//cdn.datatables.net/1.10.24/css/jquery.dataTables.min.css">
    <title>DataTables example</title>
</head>
<body>
<h1>Behold... the power of DataTables!</h1>
<table id="theTable" class="display" style="width: 100%">
    <thead>
        <tr>
            <th>Id</th>
            <th>Name</th>
            <th>Price</th>
        </tr>
    </thead>
    <tfoot>
        <tr>
            <th>Id</th>
            <th>Name</th>
            <th>Price</th>
        </tr>
    </tfoot>
</table>
</body>
</html>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://cdn.datatables.net/1.10.24/js/jquery.dataTables.min.js"></script>
<script type="application/javascript">
    $(document).ready( function () {
        $('#theTable').DataTable({
            'data': [
                [1, 'Box', 10.0],
                [2, 'Chair', 20.2]
            ],
        });
    } );
</script>

Por supuesto que si toda la información tuviese que estar hardcodeada no sería muy útil la aplicación, ¿cierto?

Una versión algo más realista sería combinar este código con algo de PHP que levante información de una db, por ejemplo:

<?php

try {
    $conn = new PDO('sqlite:dt.sq3');
} catch (PDOException $exception) {
    die($exception->getMessage());
}

$sql = "SELECT * FROM products";
$st = $conn
->query($sql);

if ($st) {
$rs = $st->fetchAll(PDO::FETCH_ASSOC );
?>
<script type="application/javascript">
    $(document).ready( function () {
        $('#theTable').DataTable({
            'data': [
                <?php
                foreach ($rs as $row) {
                    ?>
                [ '<?php echo $row['id'];?>', '<?php echo $row['name'];?>', '<?php echo $row['price'];?>' ],
                    <?php
                }
                ?>
            ],
        });
    } );
</script>
<?php
} else {
    var_dump($conn->errorInfo());
    die;
}

Hasta aquí, todo perfecto.

El problema se presenta cuando la cantidad de registros es grande.

La definición de grande es un poco esquiva… podés empezar a notar problemas con 500 registros pero si estás tratando con algo como 150 000 o más definitivamente tus páginas van a tardar una eternidad en mostrarse.

Aún si tenés páginas pequeñas (Digamos, de 10 registros cada una), el tiempo total de carga no se modifica en absoluto…

¿Por qué sucede esto?

Como en cualquier otro script, esto es, a grosso modo lo que ocurre:

  1. El servidor recibe la petición
  2. Se ejecuta el PHP
  3. Se realiza la consulta a la db
  4. El servidor recibe todos los registros de la tabla
  5. Se genera el HTML y el JavaScript
  6. Se envía todo al cliente
  7. El cliente interpreta el HTML y el JavaScript
  8. Se ejecuta el JavaScript y se dibuja la tabla y todo lo demás

Como te podrás imaginar, para trabajar con los registros de la primera página no es realmente necesario tener en memoria las otras 500…

Toda esa información que no se utilizará no hace más que sobrecargar tanto al servidor (especialmente al de bases de datos) como al cliente.

Es más, si hay muchos usuarios accediendo a la vez el problema se va multiplicando, con lo cual la experiencia de uso es peor para todos 🙁

Podrías intentar atacar el problema mejorar los recursos de hardware, implementando algún tipo de caché o similar y es posible que todo eso ayudara.

Sin embargo, existe una solución mucho más al alcance de tus manos.

La solución: Procesamiento del lado del Servidor

Y aquí viene la genialidad de DataTables… ¡lo pensaron todo estos muchachos!

Este plugin admite dos modos de procesamiento: Client-Side (Es decir, todo se hace en el navegador del cliente) o Server-Side (El procesamiento se hace del lado del servidor y sólo se realiza el rendering del lado del cliente).

El modo por defecto es, por supuesto, Client-Side.

He ahí la clave del problema.

Trabajar con Server-Side es un poco más complicado, pero tampoco es terrible.

Lo más importante es comprender que, en este modo, DataTables hará una llamada Ajax cada vez que se requiera re-dibujar la información de la tabla (Al paginar, ordenar, etc…).

Para comenzar a usar este modo necesitás cambiar la configuración de tu DataTable por algo como:

$('#theTable').DataTable( {
    serverSide: true,
    ajax: '/get_data.php'
} );

Claro que, a diferencia del ejemplo en que la tabla se carga por Ajax sólo al comienzo (o cuando se refresca), esta versión de get_data.php deberá ser bastante más inteligente para poder responder a las órdenes que le dará el FrontEnd.

Qué envía el FrontEnd al BackEnd en DataTables Server-Side

La comunicación entre FrontEnd y BackEnd se realizará a través del envío de parámetros a través de la URL de la llamada (salvo que lo cambies explícitamente).

Esto implica que tu script php deberá recogerlos desde $_GET.

A modo de resumen te comento los más importantes (La lista completa la podés encontrar acá):

start: Número de registro por el que debe comenzar la paginación

length: Cantidad de registros que se espera que devuelva el servidor. Es decir, a partir del registro start, retorná length registros (Similar al LIMIT X OFFSET Y de SQL)

search: Arreglo que contiene los parámetros de la búsqueda realizada por el usuario. Esta búsqueda es global, es decir, aplica a todas aquellas columnas marcadas como searchable.

  • value: El texto ingresado por el usuario en el cuadro de búsqueda
  • regex: Booleano que indica si el value debe ser interpretado en forma literal o como expresión regular

columns: Arreglo con una entrada por cada columna de la tabla.

Cada entrada de este arreglo será, a su vez, un arreglo con las siguientes claves:

  • data: información de mapeo entre el arreglo de datos y la tabla HTML (En este ejemplo se trata del índice que vincula la columna en la visualización con el campo en la consulta: id => 0, name => 1, price => 2)
  • name: Nombre de la columna como está definido en la propiedad columns.name (En este ejemplo no lo estamos usando así que será un string vacío).
  • searchable: Booleano que indica si es posible realizar búsquedas sobre esta columna
  • orderable: Booleano que indica si es posible realizar ordenamientos basados en esta columna
  • search: Arreglo que contiene los parámetros de la búsqueda realizada por el usuario. Similar a la búsqueda global, sólo que aplicada a esta columna en particular (Un caso algo más avanzado y no muy frecuentemente utilizado).
    • value: El texto ingresado por el usuario en el cuadro de búsqueda
    • regex: Booleano que indica si el value debe ser interpretado en forma literal o como expresión regular

order: Arreglo que especifica el criterio de ordenamiento que debe aplicarse, contendrá una entrada por cada criterio a utilizar (Para el caso de que se quieran combinar varios). Dentro de cada entrada contendrá un arreglo con dos claves:

  1. column: Número de columna por la que se quiere ordenar
  2. dir: String con la dirección del ordenamiento (asc para ascendente, desc para descendiente)

Como te podrás imaginar, la URL de la llamada puede ser algo extensa 🙂

¿No me creés? ¿Qué te parece esta?

http://localhost:8000/get_data.php?draw=1&columns%5B0%5D%5Bdata%5D=0&columns%5B0%5D%5Bname%5D=&columns%5B0%5D%5Bsearchable%5D=true&columns%5B0%5D%5Borderable%5D=true&columns%5B0%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B0%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B1%5D%5Bdata%5D=1&columns%5B1%5D%5Bname%5D=&columns%5B1%5D%5Bsearchable%5D=true&columns%5B1%5D%5Borderable%5D=true&columns%5B1%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B1%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B2%5D%5Bdata%5D=2&columns%5B2%5D%5Bname%5D=&columns%5B2%5D%5Bsearchable%5D=true&columns%5B2%5D%5Borderable%5D=true&columns%5B2%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B2%5D%5Bsearch%5D%5Bregex%5D=false&order%5B0%5D%5Bcolumn%5D=0&order%5B0%5D%5Bdir%5D=asc&start=0&length=10&search%5Bvalue%5D=&search%5Bregex%5D=false&_=1619613946473

Si la pasás a través de urldecode queda un poquito más entendible:

http://localhost:8000/get_data.php?draw=1&columns[0][data]=0&columns[0][name]=&columns[0][searchable]=true&columns[0][orderable]=true&columns[0][search][value]=&columns[0][search][regex]=false&columns[1][data]=1&columns[1][name]=&columns[1][searchable]=true&columns[1][orderable]=true&columns[1][search][value]=&columns[1][search][regex]=false&columns[2][data]=2&columns[2][name]=&columns[2][searchable]=true&columns[2][orderable]=true&columns[2][search][value]=&columns[2][search][regex]=false&order[0][column]=0&order[0][dir]=asc&start=0&length=10&search[value]=&search[regex]=false&_=1619613946473

O, todavía mejor si lo ves desde la consola del navegador:

De modo que si del lado de tu PHP hacés un var_dump($_GET) te vas a encontrar con:

array (size=7)
  'draw' => string '1' (length=1)
  'columns' => 
    array (size=3)
      0 => 
        array (size=5)
          'data' => string '0' (length=1)
          'name' => string '' (length=0)
          'searchable' => string 'true' (length=4)
          'orderable' => string 'true' (length=4)
          'search' => 
            array (size=2)
              ...
      1 => 
        array (size=5)
          'data' => string '1' (length=1)
          'name' => string '' (length=0)
          'searchable' => string 'true' (length=4)
          'orderable' => string 'true' (length=4)
          'search' => 
            array (size=2)
              ...
      2 => 
        array (size=5)
          'data' => string '2' (length=1)
          'name' => string '' (length=0)
          'searchable' => string 'true' (length=4)
          'orderable' => string 'true' (length=4)
          'search' => 
            array (size=2)
              ...
  'order' => 
    array (size=1)
      0 => 
        array (size=2)
          'column' => string '0' (length=1)
          'dir' => string 'asc' (length=3)
  'start' => string '0' (length=1)
  'length' => string '10' (length=2)
  'search' => 
    array (size=2)
      'value' => string '' (length=0)
      'regex' => string 'false' (length=5)
  '_' => string '1619615674442' (length=13)

Y de ahí… bueno, habrá que hacer la consulta que corresponda, por ejemplo:

<?php

try {
    $conn = new PDO('sqlite:dt.sq3');
} catch (PDOException $exception) {
    die($exception->getMessage());
}

$orderBy = " ORDER BY ";
foreach ($_GET['order'] as $order) {
    $orderBy .= $order['column'] + 1 . " {$order['dir']}, ";
}

$orderBy = substr($orderBy, 0, -2);
$where = '';

$columns = $_GET['columns'];
$fields = ['id', 'name', 'price'];
$where = '';

foreach ($columns as $k => $column) {
    if ($search = $column['search']['value']) {
        $where .= $fields[$k].' = '.$search.' AND ';
    }
}

$where = substr($where, 0, -5);
$length = $_GET['length'];
$start = $_GET['start'];

$sql = "SELECT * FROM products ".($where ? "WHERE $where " : '')."$orderBy LIMIT $length OFFSET $start";

(Me tomé una pequeña licencia… asumí que el search no será una expresión regular… te queda como ejercicio :))

Cómo debe responder el BackEnd al FrontEnd en DataTables Server-Side

Por otro lado, es importante que tu php sepa qué es lo que DataTables está esperando como respuesta, de otro modo la comunicación no podrá realizarse exitosamente.

Lo principal: DataTables espera un resultado JSON que debe contener:

data: El arreglo de los registros que se levantaron de la base

recordsTotal: Número total de registros que existen en la base (Sin aplicar filtros)

recordsFiltered: Número de registros que devuelve la consulta (Una vez aplicados los filtros).

En definitiva, el código php se completa con:

$countSql = "SELECT count(id) as Total FROM products";
$countSt = $conn
    ->query($countSql);

$total = $countSt->fetch()['Total'];

$sql = "SELECT * FROM products ".($where ?? "WHERE $where ")."$orderBy LIMIT $length OFFSET $start";
$st = $conn
    ->query($sql);

if ($st) {
    $rs = $st->fetchAll(PDO::FETCH_FUNC, fn($id, $name, $price) => [$id, $name, $price] );

    echo json_encode([
        'data' => $rs,
        'recordsTotal' => $total,
        'recordsFiltered' => count($rs),
    ]);
} else {
    var_dump($conn->errorInfo());
    die;
}

El código completo lo podés descargar de aquí.

DataTables ServerSide FTW!

En este artículo aprendiste las razones por las que tu página con DataTables tarda mucho en cargar y cómo podés acelerar los tiempos de carga.

¿Siempre te conviene usar ServerSide? No

Si la cantidad de registros es pequeña (O más bien diría si no hay retrasos visibles) no te conviene hacer una implementación con esta complejidad, pero en cambio, si los DataTables se están volviendo un cuello de botella SeverSide puede ser una buena solución.

Ahora que lo sabés, andá a acelerar esa página que los usuarios están esperando!

mchojrin

¿Te quedó alguna duda? Publica aca tu pregunta

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

A %d blogueros les gusta esto: